Skip to content

ngwidgets API Documentation

ai_chat

Created on 2024-02-05

based on https://raw.githubusercontent.com/zauberzeug/nicegui/main/examples/chat_app/main.py

@author: wf

ai_tasks

Created on 2025-12-08

@author: wf

AITasks

Complete AI configuration managing APIs, Models, and Tasks

Source code in ngwidgets/ai_tasks.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@lod_storable
class AITasks:
    """Complete AI configuration managing APIs, Models, and Tasks"""

    api: APIConfig = field(default_factory=APIConfig)  # General API connection settings
    models: Dict[str, ModelConfig] = field(
        default_factory=dict
    )  # Dictionary of available models, keyed by a short alias
    tasks: Dict[str, TaskConfig] = field(
        default_factory=dict
    )  # Dictionary of defined tasks, keyed by task name
    _instance: Optional["AITasks"] = None  # Singleton instance storage
    _llms: Dict[str, LLM] = field(
        default_factory=dict
    )  # Runtime cache of instantiated LLM clients

    @classmethod
    def get_instance(cls, yaml_file_path: Optional[str] = None) -> "AITasks":
        """Retrieve or create singleton instance"""
        if cls._instance is None:
            if yaml_file_path is None:
                examples_path = Path(__file__).parent.parent / "ngwidgets_examples"
                yaml_file_path = str(examples_path / "ai_tasks.yaml")

            # basemkit lod_storable load method
            cls._instance = cls.load_from_yaml_file(yaml_file_path)

        return cls._instance

    def perform_task(
        self,
        task_name: str,
        model_name: Optional[str] = None,
        prompt_override: Optional[str] = None,
        params: Dict[str, Any] = None,
    ) -> str:
        """
        Execute a configured AI task.

        Args:
            task_name: Key of the task in configuration.
            model_name: Optional override for the model key.
            prompt_override: Optional override for the prompt text.
            params: Additional parameters (e.g., image_path).

        Returns:
            str: The response text from the LLM.
        """
        if params is None:
            params = {}

        response: str = ""

        # 1. Validate task existence
        if task_name not in self.tasks:
            available = ", ".join(self.tasks.keys())
            raise ValueError(f"Unknown task '{task_name}'. Available: {available}")

        task = self.tasks[task_name]

        # 2. Determine target model
        target_model_name = model_name if model_name is not None else task.model
        if target_model_name is None:
            raise ValueError(f"No model specified for task '{task_name}'")

        # 3. Validate model existence
        if target_model_name not in self.models:
            available = ", ".join(self.models.keys())
            raise ValueError(
                f"Unknown model '{target_model_name}'. Available: {available}"
            )

        model = self.models[target_model_name]

        # 4. Verify model supports the required input type
        if task.input_type not in model.input_types:
            supported = ", ".join(model.input_types)
            raise ValueError(
                f"Model '{target_model_name}' does not support input type '{task.input_type}'. "
                f"Supported types: {supported}"
            )

        # 5. Retrieve or Initialize LLM instance
        llm_cache_key = f"{target_model_name}-{task.input_type}"

        if llm_cache_key not in self._llms:
            if task.input_type == "vision":
                self._llms[llm_cache_key] = VisionLLM(
                    model=model.name,
                    base_url=self.api.base_url,
                )
            else:
                self._llms[llm_cache_key] = LLM(
                    model=model.name,
                    base_url=self.api.base_url,
                )

        llm = self._llms[llm_cache_key]

        # 6. Execute request
        prompt_text = prompt_override if prompt_override is not None else task.prompt

        if task.input_type == "vision":
            if "image_path" not in params:
                raise ValueError(
                    f"Vision task '{task_name}' requires 'image_path' parameter"
                )
            response = llm.analyze_image(
                image_path=params["image_path"], prompt_text=prompt_text
            )
        else:
            response = llm.ask(prompt=prompt_text, model=model.name)

        return response

get_instance(yaml_file_path=None) classmethod

Retrieve or create singleton instance

Source code in ngwidgets/ai_tasks.py
106
107
108
109
110
111
112
113
114
115
116
117
@classmethod
def get_instance(cls, yaml_file_path: Optional[str] = None) -> "AITasks":
    """Retrieve or create singleton instance"""
    if cls._instance is None:
        if yaml_file_path is None:
            examples_path = Path(__file__).parent.parent / "ngwidgets_examples"
            yaml_file_path = str(examples_path / "ai_tasks.yaml")

        # basemkit lod_storable load method
        cls._instance = cls.load_from_yaml_file(yaml_file_path)

    return cls._instance

perform_task(task_name, model_name=None, prompt_override=None, params=None)

Execute a configured AI task.

Parameters:

Name Type Description Default
task_name str

Key of the task in configuration.

required
model_name Optional[str]

Optional override for the model key.

None
prompt_override Optional[str]

Optional override for the prompt text.

None
params Dict[str, Any]

Additional parameters (e.g., image_path).

None

Returns:

Name Type Description
str str

The response text from the LLM.

Source code in ngwidgets/ai_tasks.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def perform_task(
    self,
    task_name: str,
    model_name: Optional[str] = None,
    prompt_override: Optional[str] = None,
    params: Dict[str, Any] = None,
) -> str:
    """
    Execute a configured AI task.

    Args:
        task_name: Key of the task in configuration.
        model_name: Optional override for the model key.
        prompt_override: Optional override for the prompt text.
        params: Additional parameters (e.g., image_path).

    Returns:
        str: The response text from the LLM.
    """
    if params is None:
        params = {}

    response: str = ""

    # 1. Validate task existence
    if task_name not in self.tasks:
        available = ", ".join(self.tasks.keys())
        raise ValueError(f"Unknown task '{task_name}'. Available: {available}")

    task = self.tasks[task_name]

    # 2. Determine target model
    target_model_name = model_name if model_name is not None else task.model
    if target_model_name is None:
        raise ValueError(f"No model specified for task '{task_name}'")

    # 3. Validate model existence
    if target_model_name not in self.models:
        available = ", ".join(self.models.keys())
        raise ValueError(
            f"Unknown model '{target_model_name}'. Available: {available}"
        )

    model = self.models[target_model_name]

    # 4. Verify model supports the required input type
    if task.input_type not in model.input_types:
        supported = ", ".join(model.input_types)
        raise ValueError(
            f"Model '{target_model_name}' does not support input type '{task.input_type}'. "
            f"Supported types: {supported}"
        )

    # 5. Retrieve or Initialize LLM instance
    llm_cache_key = f"{target_model_name}-{task.input_type}"

    if llm_cache_key not in self._llms:
        if task.input_type == "vision":
            self._llms[llm_cache_key] = VisionLLM(
                model=model.name,
                base_url=self.api.base_url,
            )
        else:
            self._llms[llm_cache_key] = LLM(
                model=model.name,
                base_url=self.api.base_url,
            )

    llm = self._llms[llm_cache_key]

    # 6. Execute request
    prompt_text = prompt_override if prompt_override is not None else task.prompt

    if task.input_type == "vision":
        if "image_path" not in params:
            raise ValueError(
                f"Vision task '{task_name}' requires 'image_path' parameter"
            )
        response = llm.analyze_image(
            image_path=params["image_path"], prompt_text=prompt_text
        )
    else:
        response = llm.ask(prompt=prompt_text, model=model.name)

    return response

APIConfig

API configuration for LLM services

Source code in ngwidgets/ai_tasks.py
16
17
18
19
20
21
22
@lod_storable
class APIConfig:
    """API configuration for LLM services"""

    base_url: str = (
        "https://openrouter.ai/api/v1"  # The endpoint URL for the LLM API provider
    )

ModelConfig

Configuration for an LLM model

Source code in ngwidgets/ai_tasks.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@lod_storable
class ModelConfig:
    """Configuration for an LLM model"""

    name: str  # Full OpenRouter model ID (e.g., google/gemini-2.0-flash-001)
    provider: str  # The model provider organization (e.g., google, openai, anthropic)
    description: Optional[str] = (
        None  # A human-readable summary of the model's capabilities
    )
    context_length: Optional[int] = None  # Maximum context window size in tokens
    price_per_mtoken: Optional[float] = None  # cost per million tokens in USD
    input_types: List[str] = field(
        default_factory=lambda: ["text"]
    )  # List of supported modalities (e.g., ["text", "vision"])

    @classmethod
    def from_openai_model(cls, model: Any) -> "ModelConfig":
        """
        Create ModelConfig from an OpenAI API model object.

        The price per million tokens is calculated as the average of input and output costs:
        $$ P_{avg} = \frac{P_{prompt} + P_{completion}}{2} \times 1,000,000 $$
        """
        # Extract provider from model ID
        provider = model.id.split("/")[0] if "/" in model.id else "unknown"

        # Calculate price per million tokens (average of prompt + completion)
        price_per_mtoken = None
        pricing = getattr(model, "pricing", None)
        if pricing:
            avg_price = (float(pricing["prompt"]) + float(pricing["completion"])) / 2
            if avg_price > 0:
                price_per_mtoken = avg_price * 1_000_000

        # Determine input types from architecture modalities
        input_types = ["text"]  # Text is universally supported
        arch = getattr(model, "architecture", {})
        modalities = arch.get("input_modalities", [])

        for modality in modalities:
            if modality in ["image", "audio", "video"] and modality not in input_types:
                input_types.append(modality)

        config_instance = cls(
            name=model.id,
            provider=provider,
            context_length=getattr(model, "context_length", None),
            price_per_mtoken=price_per_mtoken,
            input_types=input_types,
        )
        return config_instance

from_openai_model(model) classmethod

Create ModelConfig from an OpenAI API model object.

The price per million tokens is calculated as the average of input and output costs: $$ P_{avg} = rac{P_{prompt} + P_{completion}}{2} imes 1,000,000 $$

Source code in ngwidgets/ai_tasks.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@classmethod
def from_openai_model(cls, model: Any) -> "ModelConfig":
    """
    Create ModelConfig from an OpenAI API model object.

    The price per million tokens is calculated as the average of input and output costs:
    $$ P_{avg} = \frac{P_{prompt} + P_{completion}}{2} \times 1,000,000 $$
    """
    # Extract provider from model ID
    provider = model.id.split("/")[0] if "/" in model.id else "unknown"

    # Calculate price per million tokens (average of prompt + completion)
    price_per_mtoken = None
    pricing = getattr(model, "pricing", None)
    if pricing:
        avg_price = (float(pricing["prompt"]) + float(pricing["completion"])) / 2
        if avg_price > 0:
            price_per_mtoken = avg_price * 1_000_000

    # Determine input types from architecture modalities
    input_types = ["text"]  # Text is universally supported
    arch = getattr(model, "architecture", {})
    modalities = arch.get("input_modalities", [])

    for modality in modalities:
        if modality in ["image", "audio", "video"] and modality not in input_types:
            input_types.append(modality)

    config_instance = cls(
        name=model.id,
        provider=provider,
        context_length=getattr(model, "context_length", None),
        price_per_mtoken=price_per_mtoken,
        input_types=input_types,
    )
    return config_instance

TaskConfig

Configuration for an AI task

Source code in ngwidgets/ai_tasks.py
78
79
80
81
82
83
84
85
86
87
@lod_storable
class TaskConfig:
    """Configuration for an AI task"""

    prompt: str  # The system or user prompt template to send to the model
    model: Optional[str] = (
        None  # Key reference to a specific entry in the models dictionary
    )
    description: Optional[str] = None  # Description of what this specific task achieves
    input_type: str = "text"  # The required input modality (e.g., 'text', 'vision')

axes_helper

AxesHelper

A Python class for creating a 3D helper for visualizing the axes in a scene. This class is designed to be used with the nicegui library (https://pypi.org/project/nicegui/) for creating 3D scenes. It creates line objects for the x, y, and z axes, each with a distinct color, and provides methods for changing these colors and toggling their visibility.

The original JavaScript code, which this Python code is based on, can be found at: https://raw.githubusercontent.com/mrdoob/three.js/master/src/helpers/AxesHelper.js The JavaScript code was refactored into this Python version by OpenAI's ChatGPT.

For the refactoring, following prompts were given: 1. Refactor a JavaScript class for handling ThreeJS scenes into a Python class using the nicegui library. 2. The class should allow to hide/show axes. 3. Usage of nicegui library's API for the colors. 4. Include Google docstrings and type hints to the code. 5. The scene should be a constructor parameter to be remembered. 6. The Axes should be named x, y and z. The name attribute should be assigned after the line object creation. 7. The set_colors method should be called in the constructor. 8. Use with self.scene as scene when drawing the lines. 9. The class should provide a method to toggle the visibility of axes. 10. The default size of the axes to be drawn should be 10.0. 11. Color information should be persisted when toggling the axes. 12. Use a dictionary to store colors by axes names.

According to nicegui's documentation (https://nicegui.io/documentation/scene), the 'scene.line()' function is used to create lines, here is an example: scene.line([-4, 0, 0], [-4, 2, 0]).material('#ff0000')

Date: 2023-07-24 @author: Refactored into Python by OpenAI's ChatGPT

Attributes:

Name Type Description
size float

The size of the axes to be drawn.

scene scene

The scene where the axes will be drawn.

Usage

scene = ui.scene().classes('w-full h-64') axes_helper = AxesHelper(scene, size=10.0) axes_helper.set_colors(x_axis_color='#FF0000', y_axis_color='#00FF00', z_axis_color='#0000FF') # set colors for x, y, and z axes axes_helper.toggle_axes() # toggle visibility of the axes

Source code in ngwidgets/axes_helper.py
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class AxesHelper:
    """
    A Python class for creating a 3D helper for visualizing the axes in a scene.
    This class is designed to be used with the `nicegui` library (https://pypi.org/project/nicegui/)
    for creating 3D scenes. It creates line objects for the x, y, and z axes,
    each with a distinct color, and provides methods for changing these colors and toggling their visibility.

    The original JavaScript code, which this Python code is based on, can be found at:
    https://raw.githubusercontent.com/mrdoob/three.js/master/src/helpers/AxesHelper.js
    The JavaScript code was refactored into this Python version by OpenAI's ChatGPT.

    For the refactoring, following prompts were given:
    1. Refactor a JavaScript class for handling ThreeJS scenes into a Python class using the `nicegui` library.
    2. The class should allow to hide/show axes.
    3. Usage of `nicegui` library's API for the colors.
    4. Include Google docstrings and type hints to the code.
    5. The scene should be a constructor parameter to be remembered.
    6. The Axes should be named x, y and z. The name attribute should be assigned after the line object creation.
    7. The `set_colors` method should be called in the constructor.
    8. Use `with self.scene as scene` when drawing the lines.
    9. The class should provide a method to toggle the visibility of axes.
    10. The default size of the axes to be drawn should be 10.0.
    11. Color information should be persisted when toggling the axes.
    12. Use a dictionary to store colors by axes names.

    According to nicegui's documentation (https://nicegui.io/documentation/scene), the 'scene.line()' function is used to
    create lines, here is an example:
        scene.line([-4, 0, 0], [-4, 2, 0]).material('#ff0000')

    Date: 2023-07-24
    @author: Refactored into Python by OpenAI's ChatGPT

    Attributes:
        size (float): The size of the axes to be drawn.
        scene (ui.scene): The scene where the axes will be drawn.

    Usage:
        scene = ui.scene().classes('w-full h-64')
        axes_helper = AxesHelper(scene, size=10.0)
        axes_helper.set_colors(x_axis_color='#FF0000', y_axis_color='#00FF00', z_axis_color='#0000FF')  # set colors for x, y, and z axes
        axes_helper.toggle_axes()  # toggle visibility of the axes
    """

    def __init__(self, scene: "ui.scene", size: float = 10.0):
        """
        The constructor for AxesHelper class.

        Args:
            scene (ui.scene): The scene where the axes will be drawn.
            size (float): The size of the axes to be drawn.
        """
        self.scene = scene
        self.size = size
        self.vertices = [
            (0, 0, 0),
            (size, 0, 0),
            (0, 0, 0),
            (0, size, 0),
            (0, 0, 0),
            (0, 0, size),
        ]

        self.axis_names = ["x", "y", "z"]
        self.lines = []
        self.axes_visible = False
        self.color_by_name = {"x": "#FF0000", "y": "#00FF00", "z": "#0000FF"}
        self.toggle_axes()

    def set_colors(
        self,
        x_axis_color: str = "#FF0000",
        y_axis_color: str = "#00FF00",
        z_axis_color: str = "#0000FF",
    ):
        """
        A method to set colors of the axes.

        Args:
            x_axis_color (str): Color of the x-axis.
            y_axis_color (str): Color of the y-axis.
            z_axis_color (str): Color of the z-axis.
        """
        self.color_by_name = {"x": x_axis_color, "y": y_axis_color, "z": z_axis_color}
        for idx, line in enumerate(self.lines):
            line.material(self.color_by_name[self.axis_names[idx]])

    def toggle_axes(self):
        """
        A method to toggle the visibility of the axes.
        """
        if self.axes_visible:
            # Axes are currently visible, so remove them
            for line in self.lines:
                try:
                    line.delete()
                except KeyError:
                    pass
            self.lines = []
            self.axes_visible = False
            self.scene.update()
        else:
            # Axes are currently not visible, so add them
            with self.scene:
                for i in range(3):
                    line = self.scene.line(
                        self.vertices[2 * i], self.vertices[2 * i + 1]
                    )
                    line.name = self.axis_names[i]
                    line.material(self.color_by_name[line.name])
                    self.lines.append(line)
            self.axes_visible = True

__init__(scene, size=10.0)

The constructor for AxesHelper class.

Parameters:

Name Type Description Default
scene scene

The scene where the axes will be drawn.

required
size float

The size of the axes to be drawn.

10.0
Source code in ngwidgets/axes_helper.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def __init__(self, scene: "ui.scene", size: float = 10.0):
    """
    The constructor for AxesHelper class.

    Args:
        scene (ui.scene): The scene where the axes will be drawn.
        size (float): The size of the axes to be drawn.
    """
    self.scene = scene
    self.size = size
    self.vertices = [
        (0, 0, 0),
        (size, 0, 0),
        (0, 0, 0),
        (0, size, 0),
        (0, 0, 0),
        (0, 0, size),
    ]

    self.axis_names = ["x", "y", "z"]
    self.lines = []
    self.axes_visible = False
    self.color_by_name = {"x": "#FF0000", "y": "#00FF00", "z": "#0000FF"}
    self.toggle_axes()

set_colors(x_axis_color='#FF0000', y_axis_color='#00FF00', z_axis_color='#0000FF')

A method to set colors of the axes.

Parameters:

Name Type Description Default
x_axis_color str

Color of the x-axis.

'#FF0000'
y_axis_color str

Color of the y-axis.

'#00FF00'
z_axis_color str

Color of the z-axis.

'#0000FF'
Source code in ngwidgets/axes_helper.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def set_colors(
    self,
    x_axis_color: str = "#FF0000",
    y_axis_color: str = "#00FF00",
    z_axis_color: str = "#0000FF",
):
    """
    A method to set colors of the axes.

    Args:
        x_axis_color (str): Color of the x-axis.
        y_axis_color (str): Color of the y-axis.
        z_axis_color (str): Color of the z-axis.
    """
    self.color_by_name = {"x": x_axis_color, "y": y_axis_color, "z": z_axis_color}
    for idx, line in enumerate(self.lines):
        line.material(self.color_by_name[self.axis_names[idx]])

toggle_axes()

A method to toggle the visibility of the axes.

Source code in ngwidgets/axes_helper.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def toggle_axes(self):
    """
    A method to toggle the visibility of the axes.
    """
    if self.axes_visible:
        # Axes are currently visible, so remove them
        for line in self.lines:
            try:
                line.delete()
            except KeyError:
                pass
        self.lines = []
        self.axes_visible = False
        self.scene.update()
    else:
        # Axes are currently not visible, so add them
        with self.scene:
            for i in range(3):
                line = self.scene.line(
                    self.vertices[2 * i], self.vertices[2 * i + 1]
                )
                line.name = self.axis_names[i]
                line.material(self.color_by_name[line.name])
                self.lines.append(line)
        self.axes_visible = True

base_llm

Created on 2025-04-07

@author: wf

BaseLLM

Large Language Model access wrapper

Source code in ngwidgets/base_llm.py
94
95
96
97
class BaseLLM:
    """
    Large Language Model access wrapper
    """

Prompt

a single prompt with response

Source code in ngwidgets/base_llm.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@lod_storable
class Prompt:
    """
    a single prompt with response
    """

    prompt: str
    response: str
    tokens: int
    temperature: float
    timestamp: datetime
    duration: float
    model: Optional[str] = None
    model_details: Optional[str] = None
    image_path: Optional[str] = None

    def append_to_file(self, filepath: str):
        # Open the file in append mode
        with open(filepath, "a") as file:
            # Dump the new prompt as YAML directly into the file
            yaml_str = self.to_yaml()
            # Ensure the new prompt starts on a new line
            file.write(f"\n{yaml_str}")

    @classmethod
    def from_chat(
        self,
        prompt_text: str,
        result: str,
        chat_completion,
        start_time: datetime,
        model: Optional[str] = None,
        image_path: Optional[str] = None,
        temperature: float = 0.7,
    ) -> "Prompt":
        """
        Create a prompt instance with timing and usage info
        """
        total_tokens = chat_completion.usage.total_tokens
        model_details = chat_completion.model
        duration = (datetime.now() - start_time).total_seconds()
        prompt = Prompt(
            prompt=prompt_text,
            response=result,
            model=model,
            model_details=model_details,
            temperature=temperature,
            tokens=total_tokens,
            timestamp=datetime.now(),
            duration=duration,
            image_path=image_path,
        )
        return prompt

from_chat(prompt_text, result, chat_completion, start_time, model=None, image_path=None, temperature=0.7) classmethod

Create a prompt instance with timing and usage info

Source code in ngwidgets/base_llm.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@classmethod
def from_chat(
    self,
    prompt_text: str,
    result: str,
    chat_completion,
    start_time: datetime,
    model: Optional[str] = None,
    image_path: Optional[str] = None,
    temperature: float = 0.7,
) -> "Prompt":
    """
    Create a prompt instance with timing and usage info
    """
    total_tokens = chat_completion.usage.total_tokens
    model_details = chat_completion.model
    duration = (datetime.now() - start_time).total_seconds()
    prompt = Prompt(
        prompt=prompt_text,
        response=result,
        model=model,
        model_details=model_details,
        temperature=temperature,
        tokens=total_tokens,
        timestamp=datetime.now(),
        duration=duration,
        image_path=image_path,
    )
    return prompt

Prompts

keep track of a series of prompts

Source code in ngwidgets/base_llm.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@lod_storable
class Prompts:
    """
    keep track of a series of prompts
    """

    prompts: List[Prompt] = field(default_factory=list)

    def add_to_filepath(
        self, new_prompt: Prompt, prompts_filepath, debug: bool = False
    ):
        start_save_time = time.time()

        # Save the prompts to a file
        self.prompts.append(new_prompt)
        new_prompt.append_to_file(prompts_filepath)

        # After saving
        end_save_time = time.time()
        save_duration = end_save_time - start_save_time
        if debug:
            print(f"Time taken to append to prompts: {save_duration} seconds")

basetest

Created on 2021-08-19

@author: wf

Basetest

Bases: TestCase

base test case

Source code in ngwidgets/basetest.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
class Basetest(unittest.TestCase):
    """
    base test case
    """

    def setUp(self, debug=False, profile=True):
        """
        setUp test environment
        """
        unittest.TestCase.setUp(self)
        self.debug = debug
        self.profile = profile
        msg = f"test {self._testMethodName}, debug={self.debug}"
        self.profiler = Profiler(msg, profile=self.profile)

    def tearDown(self):
        unittest.TestCase.tearDown(self)
        self.profiler.time()

    @staticmethod
    def inPublicCI():
        """
        are we running in a public Continuous Integration Environment?
        """
        publicCI = getpass.getuser() in ["travis", "runner"]
        jenkins = "JENKINS_HOME" in os.environ
        return publicCI or jenkins

    @staticmethod
    def isUser(name: str):
        """Checks if the system has the given name"""
        return getpass.getuser() == name

    @staticmethod
    def timeout(seconds):
        """
        Decorator to enforce a timeout on test methods.

        params:
          1: seconds: Timeout in seconds
        """

        def decorator(func):
            @wraps(func)
            def wrapper(*args, **kwargs):
                result = [None]
                exception = [None]

                def target():
                    try:
                        result[0] = func(*args, **kwargs)
                    except Exception as e:
                        exception[0] = e

                thread = threading.Thread(target=target)
                thread.start()
                thread.join(seconds)

                if thread.is_alive():
                    raise TimeoutError(f"Test timed out after {seconds} seconds")

                if exception[0] is not None:
                    raise exception[0]

                return result[0]

            return wrapper

        return decorator

inPublicCI() staticmethod

are we running in a public Continuous Integration Environment?

Source code in ngwidgets/basetest.py
35
36
37
38
39
40
41
42
@staticmethod
def inPublicCI():
    """
    are we running in a public Continuous Integration Environment?
    """
    publicCI = getpass.getuser() in ["travis", "runner"]
    jenkins = "JENKINS_HOME" in os.environ
    return publicCI or jenkins

isUser(name) staticmethod

Checks if the system has the given name

Source code in ngwidgets/basetest.py
44
45
46
47
@staticmethod
def isUser(name: str):
    """Checks if the system has the given name"""
    return getpass.getuser() == name

setUp(debug=False, profile=True)

setUp test environment

Source code in ngwidgets/basetest.py
21
22
23
24
25
26
27
28
29
def setUp(self, debug=False, profile=True):
    """
    setUp test environment
    """
    unittest.TestCase.setUp(self)
    self.debug = debug
    self.profile = profile
    msg = f"test {self._testMethodName}, debug={self.debug}"
    self.profiler = Profiler(msg, profile=self.profile)

timeout(seconds) staticmethod

Decorator to enforce a timeout on test methods.

Parameters:

Name Type Description Default
1

seconds: Timeout in seconds

required
Source code in ngwidgets/basetest.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@staticmethod
def timeout(seconds):
    """
    Decorator to enforce a timeout on test methods.

    params:
      1: seconds: Timeout in seconds
    """

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            result = [None]
            exception = [None]

            def target():
                try:
                    result[0] = func(*args, **kwargs)
                except Exception as e:
                    exception[0] = e

            thread = threading.Thread(target=target)
            thread.start()
            thread.join(seconds)

            if thread.is_alive():
                raise TimeoutError(f"Test timed out after {seconds} seconds")

            if exception[0] is not None:
                raise exception[0]

            return result[0]

        return wrapper

    return decorator

cmd

Created on 2023-09-10

@author: wf

WebserverCmd

Bases: BaseCmd

Baseclass for command line handling of Webservers

Source code in ngwidgets/cmd.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
class WebserverCmd(BaseCmd):
    """
    Baseclass for command line handling of Webservers
    """

    def __init__(self, config: WebserverConfig, webserver_cls, debug: bool = False):
        """
        constructor
        """
        super().__init__(config.version, config.version.description)
        self.config = config
        self.version = config.version
        self.debug = debug
        self.webserver_cls = webserver_cls

    def add_arguments(self, parser: ArgumentParser):
        """
        Add webserver-specific CLI arguments.

        Args:
            parser (ArgumentParser): the parser to add arguments to.
        """
        super().add_arguments(parser)
        parser.add_argument(
            "--apache", help="create an apache configuration file for the given domain"
        )
        parser.add_argument("-c", "--client", action="store_true", help="start client")
        parser.add_argument(
            "-l",
            "--local",
            action="store_true",
            help="run with local file system access",
        )
        parser.add_argument("-i", "--input", help="input file")
        parser.add_argument(
            "-rol", "--render_on_load", action="store_true", help="render on load"
        )
        parser.add_argument(
            "--host",
            default="localhost",
            help="the host to serve / listen from (default: localhost)",
        )
        parser.add_argument(
            "--port",
            type=int,
            default=self.config.default_port,
            help=f"the port to serve from (default: {self.config.default_port})",
        )
        parser.add_argument(
            "-s", "--serve", action="store_true", help="start webserver"
        )

    def handle_args(self, args) -> bool:
        """
        Handle the given command line arguments.

        Args:
            args: The parsed command line arguments.

        Returns:
            bool: True if any argument was handled, False otherwise.
        """
        handled = super().handle_args(args)
        if args.apache:
            print(self.to_apache_config(self.config, args.apache))
            handled = True
        if args.client:
            url = f"http://{args.host}:{args.port}"
            webbrowser.open(url)
            handled = True
        if args.serve:
            ws = self.webserver_cls()
            ws.run(args)
            handled = True
        return handled

    def to_apache_config(self, config: WebserverConfig, domain: str) -> str:
        """
        Generate Apache configuration based on the given WebserverConfig.

        Args:
            config (WebserverConfig): The webserver configuration object.
            domain(str): the base domain to use
        Returns:
            str: The Apache configuration as a string.
        """
        iso_timestamp = datetime.now().isoformat()
        server_name = f"{config.short_name}.{domain}"
        admin_email = f"webmaster@{domain}"
        version_info = ""
        if config.version:
            version_info = f"""{config.version.name} Version {config.version.version} of {config.version.updated} ({config.version.description})"""

        header_comment = f"""# Apache Configuration for {server_name}
# {version_info}
# Generated by WebserverCmd at {iso_timestamp}
# http Port: {config.default_port}
# SSL Port: 443
# {config.copy_right}
# timeout: {config.timeout}
"""

        template = """<VirtualHost *:{port}>
    ServerName {server_name}
    ServerAdmin {admin_email}

    {ssl_config_part}
    ErrorLog ${{APACHE_LOG_DIR}}/{short_name}_error{log_suffix}.log
    CustomLog ${{APACHE_LOG_DIR}}/{short_name}{log_suffix}.log combined

    RewriteEngine On
    RewriteCond %{{HTTP:Upgrade}} =websocket [NC]
    RewriteRule /(.*) ws://localhost:{default_port}/$1 [P,L]
    RewriteCond %{{HTTP:Upgrade}} !=websocket [NC]
    RewriteRule /(.*) http://localhost:{default_port}/$1 [P,L]

    ProxyPassReverse / http://localhost:{default_port}/
</VirtualHost>
"""

        # For SSL Configuration
        ssl_config = template.format(
            port=443,
            server_name=server_name,
            admin_email=admin_email,
            short_name=config.short_name,
            log_suffix="_ssl",
            default_port=config.default_port,
            ssl_config_part="Include ssl.conf",
        )

        # For Non-SSL Configuration
        http_config = template.format(
            port=80,
            server_name=server_name,
            admin_email=admin_email,
            short_name=config.short_name,
            log_suffix="",
            default_port=config.default_port,
            ssl_config_part="",
        )

        apache_config = header_comment + ssl_config + http_config
        return apache_config

    def cmd_main(self, argv: list = None) -> int:
        """
        Main entry point (legacy contract).
        """
        exit_code = self.run(argv)
        return exit_code

    @classmethod
    def main(cls, config: WebserverConfig, webserver_cls, argv=None) -> int:
        """
        Main entry point to launch the webserver cmd with BaseCmd logic.
        """
        instance = cls(config=config, webserver_cls=webserver_cls)
        return instance.run(argv)

__init__(config, webserver_cls, debug=False)

constructor

Source code in ngwidgets/cmd.py
21
22
23
24
25
26
27
28
29
def __init__(self, config: WebserverConfig, webserver_cls, debug: bool = False):
    """
    constructor
    """
    super().__init__(config.version, config.version.description)
    self.config = config
    self.version = config.version
    self.debug = debug
    self.webserver_cls = webserver_cls

add_arguments(parser)

Add webserver-specific CLI arguments.

Parameters:

Name Type Description Default
parser ArgumentParser

the parser to add arguments to.

required
Source code in ngwidgets/cmd.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def add_arguments(self, parser: ArgumentParser):
    """
    Add webserver-specific CLI arguments.

    Args:
        parser (ArgumentParser): the parser to add arguments to.
    """
    super().add_arguments(parser)
    parser.add_argument(
        "--apache", help="create an apache configuration file for the given domain"
    )
    parser.add_argument("-c", "--client", action="store_true", help="start client")
    parser.add_argument(
        "-l",
        "--local",
        action="store_true",
        help="run with local file system access",
    )
    parser.add_argument("-i", "--input", help="input file")
    parser.add_argument(
        "-rol", "--render_on_load", action="store_true", help="render on load"
    )
    parser.add_argument(
        "--host",
        default="localhost",
        help="the host to serve / listen from (default: localhost)",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=self.config.default_port,
        help=f"the port to serve from (default: {self.config.default_port})",
    )
    parser.add_argument(
        "-s", "--serve", action="store_true", help="start webserver"
    )

cmd_main(argv=None)

Main entry point (legacy contract).

Source code in ngwidgets/cmd.py
161
162
163
164
165
166
def cmd_main(self, argv: list = None) -> int:
    """
    Main entry point (legacy contract).
    """
    exit_code = self.run(argv)
    return exit_code

handle_args(args)

Handle the given command line arguments.

Parameters:

Name Type Description Default
args

The parsed command line arguments.

required

Returns:

Name Type Description
bool bool

True if any argument was handled, False otherwise.

Source code in ngwidgets/cmd.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def handle_args(self, args) -> bool:
    """
    Handle the given command line arguments.

    Args:
        args: The parsed command line arguments.

    Returns:
        bool: True if any argument was handled, False otherwise.
    """
    handled = super().handle_args(args)
    if args.apache:
        print(self.to_apache_config(self.config, args.apache))
        handled = True
    if args.client:
        url = f"http://{args.host}:{args.port}"
        webbrowser.open(url)
        handled = True
    if args.serve:
        ws = self.webserver_cls()
        ws.run(args)
        handled = True
    return handled

main(config, webserver_cls, argv=None) classmethod

Main entry point to launch the webserver cmd with BaseCmd logic.

Source code in ngwidgets/cmd.py
168
169
170
171
172
173
174
@classmethod
def main(cls, config: WebserverConfig, webserver_cls, argv=None) -> int:
    """
    Main entry point to launch the webserver cmd with BaseCmd logic.
    """
    instance = cls(config=config, webserver_cls=webserver_cls)
    return instance.run(argv)

to_apache_config(config, domain)

Generate Apache configuration based on the given WebserverConfig.

Parameters:

Name Type Description Default
config WebserverConfig

The webserver configuration object.

required
domain str

the base domain to use

required

Returns: str: The Apache configuration as a string.

Source code in ngwidgets/cmd.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
    def to_apache_config(self, config: WebserverConfig, domain: str) -> str:
        """
        Generate Apache configuration based on the given WebserverConfig.

        Args:
            config (WebserverConfig): The webserver configuration object.
            domain(str): the base domain to use
        Returns:
            str: The Apache configuration as a string.
        """
        iso_timestamp = datetime.now().isoformat()
        server_name = f"{config.short_name}.{domain}"
        admin_email = f"webmaster@{domain}"
        version_info = ""
        if config.version:
            version_info = f"""{config.version.name} Version {config.version.version} of {config.version.updated} ({config.version.description})"""

        header_comment = f"""# Apache Configuration for {server_name}
# {version_info}
# Generated by WebserverCmd at {iso_timestamp}
# http Port: {config.default_port}
# SSL Port: 443
# {config.copy_right}
# timeout: {config.timeout}
"""

        template = """<VirtualHost *:{port}>
    ServerName {server_name}
    ServerAdmin {admin_email}

    {ssl_config_part}
    ErrorLog ${{APACHE_LOG_DIR}}/{short_name}_error{log_suffix}.log
    CustomLog ${{APACHE_LOG_DIR}}/{short_name}{log_suffix}.log combined

    RewriteEngine On
    RewriteCond %{{HTTP:Upgrade}} =websocket [NC]
    RewriteRule /(.*) ws://localhost:{default_port}/$1 [P,L]
    RewriteCond %{{HTTP:Upgrade}} !=websocket [NC]
    RewriteRule /(.*) http://localhost:{default_port}/$1 [P,L]

    ProxyPassReverse / http://localhost:{default_port}/
</VirtualHost>
"""

        # For SSL Configuration
        ssl_config = template.format(
            port=443,
            server_name=server_name,
            admin_email=admin_email,
            short_name=config.short_name,
            log_suffix="_ssl",
            default_port=config.default_port,
            ssl_config_part="Include ssl.conf",
        )

        # For Non-SSL Configuration
        http_config = template.format(
            port=80,
            server_name=server_name,
            admin_email=admin_email,
            short_name=config.short_name,
            log_suffix="",
            default_port=config.default_port,
            ssl_config_part="",
        )

        apache_config = header_comment + ssl_config + http_config
        return apache_config

color_map

Created on 2024-07-17

@author: wf

ColorMap

A map of color ranges.

the first column of each row has the color range from start_color to end_color the other columns are interpolated using the lum_min, lum_max and sat_f parameters

Attributes:

Name Type Description
start_color Color

The starting color of the range.

end_color Color

The ending color of the range.

num_levels int

The number of levels in the color range.

lum_min float

The minimum luminance

lum_max float

The maximum luminance

sat_f float

The saturation factor

Source code in ngwidgets/color_map.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ColorMap:
    """
    A map of color ranges.

    the first column of each row has the color range from start_color to end_color
    the other columns are interpolated using the lum_min, lum_max and sat_f parameters

    Attributes:
        start_color (Color): The starting color of the range.
        end_color (Color): The ending color of the range.
        num_levels (int): The number of levels in the color range.
        lum_min (float): The minimum luminance
        lum_max (float): The maximum luminance
        sat_f (float): The saturation factor
    """

    def __init__(
        self,
        start_color: str = "#ff002b",
        end_color: str = "#110080",
        num_levels: int = 5,
        lum_min: float = 0.5,
        lum_max: float = 0.86,
        sat_f: float = 0.35,
    ):
        self.start_color = Color(start_color)
        self.end_color = Color(end_color)
        self.num_levels = num_levels
        self.lum_min = lum_min
        self.lum_max = lum_max
        self.sat_f = sat_f
        self.main_colors = list(self.start_color.range_to(self.end_color, num_levels))
        self.color_matrix = self._create_color_matrix()

    def _map_to_color(self, main_color: Color, col_index: float) -> Color:
        """
        Maps a column index to a color shade based on the main color.
        """
        luminance = (self.lum_max - self.lum_min) * col_index + self.lum_min
        saturation = main_color.saturation * (1 - (1 - self.sat_f) * col_index)
        color = Color(hue=main_color.hue, saturation=saturation, luminance=luminance)
        return color

    def _create_color_matrix(self):
        matrix = []
        for main_color in self.main_colors:
            shade_row = []
            for col in range(self.num_levels):
                col_index = col / (self.num_levels - 1)
                shade = self._map_to_color(main_color, col_index)
                shade_row.append(shade)
            matrix.append(shade_row)
        return matrix

    def get_color(self, row: int, col: int) -> Color:
        """
        Retrieves the color hex code at the specified row and column indices.
        """
        return self.color_matrix[row][col]

get_color(row, col)

Retrieves the color hex code at the specified row and column indices.

Source code in ngwidgets/color_map.py
64
65
66
67
68
def get_color(self, row: int, col: int) -> Color:
    """
    Retrieves the color hex code at the specified row and column indices.
    """
    return self.color_matrix[row][col]

color_schema

Created on 2023-09-13

@author: wf

ColorSchema dataclass

a nicegui color schema

Source code in ngwidgets/color_schema.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
@dataclass
class ColorSchema:
    """
    a nicegui color schema
    """

    name: str = "default"
    primary: str = "#5898d4"
    secondary: str = "#26a69a"
    accent: str = "#9c27b0"
    dark: str = "#1d1d1d"
    positive: str = "#21ba45"
    negative: str = "#c10015"
    info: str = "#31ccec"
    warning: str = "#f2c037"

    def apply(self):
        """
        Apply this color schema to the current UI theme.
        """
        ui.colors(
            primary=self.primary,
            secondary=self.secondary,
            accent=self.accent,
            dark=self.dark,
            positive=self.positive,
            negative=self.negative,
            info=self.info,
            warning=self.warning,
        )

    @classmethod
    def get_schemas(cls):
        """
        Return a list of all available color schemas.
        """
        return [
            cls.default(),
            cls.indigo(),
            cls.red(),
            cls.pink_red(),
            cls.purple(),
            cls.deep_purple(),
            cls.blue(),
            cls.light_blue(),
            cls.cyan(),
            cls.teal(),
            cls.green(),
            cls.light_green(),
            cls.lime(),
            cls.yellow(),
            cls.amber(),
            cls.orange(),
            cls.deep_orange(),
            cls.brown(),
            cls.grey(),
            cls.blue_grey(),
        ]

    @classmethod
    def default(cls):
        """
        Return the default color schema.
        """
        return cls()

    def display(self):
        """
        Display all available color schemas visually in the UI.
        """
        for schema in ColorSchema.get_schemas():
            style = (
                "color: white;"
                "width: 75px; "
                "height: 50px; "
                "border: 1px solid #000; "
                "display: flex; "
                "justify-content: center; "
                "align-items: center; "
                "border-radius: 5px;"
            )
            with ui.row().style("margin-bottom: 10px;"):
                ui.label(schema.name).style(style + "background:grey;")
                schema._display_color("Primary", schema.primary, style)
                schema._display_color("Secondary", schema.secondary, style)
                schema._display_color("Accent", schema.accent, style)
                schema._display_color("Dark", schema.dark, style)
                schema._display_color("Positive", schema.positive, style)
                schema._display_color("Negative", schema.negative, style)
                schema._display_color("Info", schema.info, style)
                schema._display_color("Warning", schema.warning, style)

    def _display_color(self, name: str, color: str, style: str):
        with ui.column():
            ui.label(name).style(style + f"background: {color};")

    @classmethod
    def blue_grey(cls):
        """
        Return a color schema for the Blue Grey color palette from Material Palette.
        see https://www.materialpalette.com/grey/blue-grey
        """
        return cls(
            name="blue_grey",
            primary="#607D8B",  # Blue Grey
            secondary="#B0BEC5",  # Light Blue Grey
            accent="#37474F",  # Dark Blue Grey
            dark="#263238",  # Deepest Blue Grey
            positive="#4CAF50",  # Standard Positive Green
            negative="#D32F2F",  # Standard Negative Red
            info="#2196F3",  # Standard Info Blue
            warning="#FFC107",  # Standard Warning Amber
        )

    @classmethod
    def red(cls):
        """
        Return a color schema for the Red color palette from Material Palette.
        see https://www.materialpalette.com/red/red
        """
        return cls(
            name="red",
            primary="#F44336",
            secondary="#FFCDD2",
            accent="#D32F2F",
            dark="#B71C1C",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def purple(cls):
        """
        Return a color schema for the Purple color palette from Material Palette.
        see https://www.materialpalette.com/purple/purple
        """
        return cls(
            name="purple",
            primary="#9C27B0",  # Purple
            secondary="#CE93D8",  # Light Purple
            accent="#7B1FA2",  # Dark Purple
            dark="#4A148C",  # Deepest Purple
            positive="#4CAF50",  # Standard Positive Green
            negative="#D32F2F",  # Standard Negative Red
            info="#2196F3",  # Standard Info Blue
            warning="#FFC107",  # Standard Warning Amber
        )

    @classmethod
    def deep_purple(cls):
        """
        Return a color schema for the Deep Purple color palette from Material Palette.
        see https://www.materialpalette.com/purple/deep-purple
        """
        return cls(
            name="deep_purple",
            primary="#673AB7",  # Deep Purple
            secondary="#9575CD",  # Light Deep Purple
            accent="#512DA8",  # Dark Deep Purple
            dark="#311B92",  # Deepest Deep Purple
            positive="#4CAF50",  # Standard Positive Green
            negative="#D32F2F",  # Standard Negative Red
            info="#2196F3",  # Standard Info Blue
            warning="#FFC107",  # Standard Warning Amber
        )

    @classmethod
    def blue(cls):
        """
        Return a color schema for the Blue color palette from Material Palette.
        see https://www.materialpalette.com/blue/blue
        """
        return cls(
            name="blue",
            primary="#2196F3",  # Blue
            secondary="#90CAF9",  # Light Blue
            accent="#1976D2",  # Dark Blue
            dark="#0D47A1",  # Deepest Blue
            positive="#4CAF50",  # Standard Positive Green
            negative="#D32F2F",  # Standard Negative Red
            info="#2196F3",  # Standard Info Blue
            warning="#FFC107",  # Standard Warning Amber
        )

    @classmethod
    def light_blue(cls):
        """
        Return a color schema for the Light Blue color palette from Material Palette.
        see https://www.materialpalette.com/blue/light-blue
        """
        return cls(
            name="light_blue",
            primary="#03A9F4",  # Light Blue
            secondary="#81D4FA",  # Lightest Blue
            accent="#0288D1",  # Dark Light Blue
            dark="#01579B",  # Deepest Light Blue
            positive="#4CAF50",  # Standard Positive Green
            negative="#D32F2F",  # Standard Negative Red
            info="#2196F3",  # Standard Info Blue
            warning="#FFC107",  # Standard Warning Amber
        )

    @classmethod
    def cyan(cls):
        """
        Return a color schema for the Cyan color palette from Material Palette.
        see https://www.materialpalette.com/blue/cyan
        """
        return cls(
            name="cyan",
            primary="#00BCD4",  # Cyan
            secondary="#80DEEA",  # Light Cyan
            accent="#0097A7",  # Dark Cyan
            dark="#006064",  # Deepest Cyan
            positive="#4CAF50",  # Standard Positive Green
            negative="#D32F2F",  # Standard Negative Red
            info="#2196F3",  # Standard Info Blue
            warning="#FFC107",  # Standard Warning Amber
        )

    @classmethod
    def pink_red(cls):
        """
        Return a color schema for the Pink/Red color palette from Material Palette.
        see https://www.materialpalette.com/pink/red
        """
        return cls(
            name="pink_red",
            primary="#E91E63",  # Pink
            secondary="#F8BBD0",  # Light Pink
            accent="#C2185B",  # Dark Pink
            dark="#880E4F",  # Deepest Pink
            positive="#4CAF50",  # Standard Positive Green
            negative="#D32F2F",  # Standard Negative Red
            info="#2196F3",  # Standard Info Blue
            warning="#FFC107",  # Standard Warning Amber
        )

    @classmethod
    def teal(cls):
        """
        Return a color schema for the Teal color palette from Material Palette.
        see https://www.materialpalette.com/teal/teal
        """
        return cls(
            name="teal",
            primary="#009688",
            secondary="#80CBC4",
            accent="#00796B",
            dark="#004D40",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def lime(cls):
        """
        Return a color schema for the Lime color palette from Material Palette.
        see https://www.materialpalette.com/lime/lime
        """
        return cls(
            name="lime",
            primary="#CDDC39",
            secondary="#F0F4C3",
            accent="#AFB42B",
            dark="#827717",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def indigo(cls):
        """
        Return a color schema for the Indigo color palette from Material Palette.
        see https://www.materialpalette.com/indigo/indigo
        """
        color_schema = cls(
            name="indigo",
            primary="#3F51B5",
            secondary="#5C6BC0",
            accent="#8A72AC",
            dark="#1A237E",
            positive="#28A745",
            negative="#D32F2F",
            info="#536DFE",
            warning="#FFB74D",
        )
        return color_schema

    @classmethod
    def light_green(cls):
        """
        Return a color schema for the Light Green color palette from Material Palette.
        see https://www.materialpalette.com/green/light-green
        """
        return cls(
            name="light_green",
            primary="#8BC34A",  # Light Green
            secondary="#DCEDC8",  # Lightest Green
            accent="#689F38",  # Dark Light Green
            dark="#33691E",  # Deepest Light Green
            positive="#4CAF50",  # Standard Positive Green
            negative="#D32F2F",  # Standard Negative Red
            info="#2196F3",  # Standard Info Blue
            warning="#FFC107",  # Standard Warning Amber
        )

    @classmethod
    def green(cls):
        """
        Return a color schema for the Green color palette from Material Palette.
        see https://www.materialpalette.com/green/green
        """
        return cls(
            name="green",
            primary="#4CAF50",
            secondary="#C8E6C9",
            accent="#388E3C",
            dark="#1B5E20",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def yellow(cls):
        """
        Return a color schema for the Yellow color palette from Material Palette.
        see https://www.materialpalette.com/yellow/yellow
        """
        return cls(
            name="yellow",
            primary="#FFEB3B",
            secondary="#FFF9C4",
            accent="#FBC02D",
            dark="#F57F17",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def amber(cls):
        """
        Return a color schema for the Amber color palette from Material Palette.
        see https://www.materialpalette.com/amber/amber
        """
        return cls(
            name="amber",
            primary="#FFC107",
            secondary="#FFECB3",
            accent="#FFA000",
            dark="#FF8F00",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def orange(cls):
        """
        Return a color schema for the Orange color palette from Material Palette.
        see https://www.materialpalette.com/orange/orange
        """
        return cls(
            name="orange",
            primary="#FF9800",
            secondary="#FFE0B2",
            accent="#FF5722",
            dark="#E64A19",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def deep_orange(cls):
        """
        Return a color schema for the Deep Orange color palette from Material Palette.
        see https://www.materialpalette.com/orange/deep-orange
        """
        return cls(
            name="deep_orange",
            primary="#FF5722",
            secondary="#FFCCBC",
            accent="#E64A19",
            dark="#BF360C",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def brown(cls):
        """
        Return a color schema for the Brown color palette from Material Palette.
        see https://www.materialpalette.com/brown/brown
        """
        return cls(
            name="brown",
            primary="#795548",
            secondary="#D7CCC8",
            accent="#5D4037",
            dark="#3E2723",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

    @classmethod
    def grey(cls):
        """
        Return a color schema for the Grey color palette from Material Palette.
        see https://www.materialpalette.com/grey/grey
        """
        return cls(
            name="grey",
            primary="#9E9E9E",
            secondary="#F5F5F5",
            accent="#616161",
            dark="#212121",
            positive="#4CAF50",
            negative="#D32F2F",
            info="#2196F3",
            warning="#FFC107",
        )

amber() classmethod

Return a color schema for the Amber color palette from Material Palette. see https://www.materialpalette.com/amber/amber

Source code in ngwidgets/color_schema.py
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
@classmethod
def amber(cls):
    """
    Return a color schema for the Amber color palette from Material Palette.
    see https://www.materialpalette.com/amber/amber
    """
    return cls(
        name="amber",
        primary="#FFC107",
        secondary="#FFECB3",
        accent="#FFA000",
        dark="#FF8F00",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

apply()

Apply this color schema to the current UI theme.

Source code in ngwidgets/color_schema.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def apply(self):
    """
    Apply this color schema to the current UI theme.
    """
    ui.colors(
        primary=self.primary,
        secondary=self.secondary,
        accent=self.accent,
        dark=self.dark,
        positive=self.positive,
        negative=self.negative,
        info=self.info,
        warning=self.warning,
    )

blue() classmethod

Return a color schema for the Blue color palette from Material Palette. see https://www.materialpalette.com/blue/blue

Source code in ngwidgets/color_schema.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
@classmethod
def blue(cls):
    """
    Return a color schema for the Blue color palette from Material Palette.
    see https://www.materialpalette.com/blue/blue
    """
    return cls(
        name="blue",
        primary="#2196F3",  # Blue
        secondary="#90CAF9",  # Light Blue
        accent="#1976D2",  # Dark Blue
        dark="#0D47A1",  # Deepest Blue
        positive="#4CAF50",  # Standard Positive Green
        negative="#D32F2F",  # Standard Negative Red
        info="#2196F3",  # Standard Info Blue
        warning="#FFC107",  # Standard Warning Amber
    )

blue_grey() classmethod

Return a color schema for the Blue Grey color palette from Material Palette. see https://www.materialpalette.com/grey/blue-grey

Source code in ngwidgets/color_schema.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@classmethod
def blue_grey(cls):
    """
    Return a color schema for the Blue Grey color palette from Material Palette.
    see https://www.materialpalette.com/grey/blue-grey
    """
    return cls(
        name="blue_grey",
        primary="#607D8B",  # Blue Grey
        secondary="#B0BEC5",  # Light Blue Grey
        accent="#37474F",  # Dark Blue Grey
        dark="#263238",  # Deepest Blue Grey
        positive="#4CAF50",  # Standard Positive Green
        negative="#D32F2F",  # Standard Negative Red
        info="#2196F3",  # Standard Info Blue
        warning="#FFC107",  # Standard Warning Amber
    )

brown() classmethod

Return a color schema for the Brown color palette from Material Palette. see https://www.materialpalette.com/brown/brown

Source code in ngwidgets/color_schema.py
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
@classmethod
def brown(cls):
    """
    Return a color schema for the Brown color palette from Material Palette.
    see https://www.materialpalette.com/brown/brown
    """
    return cls(
        name="brown",
        primary="#795548",
        secondary="#D7CCC8",
        accent="#5D4037",
        dark="#3E2723",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

cyan() classmethod

Return a color schema for the Cyan color palette from Material Palette. see https://www.materialpalette.com/blue/cyan

Source code in ngwidgets/color_schema.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
@classmethod
def cyan(cls):
    """
    Return a color schema for the Cyan color palette from Material Palette.
    see https://www.materialpalette.com/blue/cyan
    """
    return cls(
        name="cyan",
        primary="#00BCD4",  # Cyan
        secondary="#80DEEA",  # Light Cyan
        accent="#0097A7",  # Dark Cyan
        dark="#006064",  # Deepest Cyan
        positive="#4CAF50",  # Standard Positive Green
        negative="#D32F2F",  # Standard Negative Red
        info="#2196F3",  # Standard Info Blue
        warning="#FFC107",  # Standard Warning Amber
    )

deep_orange() classmethod

Return a color schema for the Deep Orange color palette from Material Palette. see https://www.materialpalette.com/orange/deep-orange

Source code in ngwidgets/color_schema.py
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
@classmethod
def deep_orange(cls):
    """
    Return a color schema for the Deep Orange color palette from Material Palette.
    see https://www.materialpalette.com/orange/deep-orange
    """
    return cls(
        name="deep_orange",
        primary="#FF5722",
        secondary="#FFCCBC",
        accent="#E64A19",
        dark="#BF360C",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

deep_purple() classmethod

Return a color schema for the Deep Purple color palette from Material Palette. see https://www.materialpalette.com/purple/deep-purple

Source code in ngwidgets/color_schema.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
@classmethod
def deep_purple(cls):
    """
    Return a color schema for the Deep Purple color palette from Material Palette.
    see https://www.materialpalette.com/purple/deep-purple
    """
    return cls(
        name="deep_purple",
        primary="#673AB7",  # Deep Purple
        secondary="#9575CD",  # Light Deep Purple
        accent="#512DA8",  # Dark Deep Purple
        dark="#311B92",  # Deepest Deep Purple
        positive="#4CAF50",  # Standard Positive Green
        negative="#D32F2F",  # Standard Negative Red
        info="#2196F3",  # Standard Info Blue
        warning="#FFC107",  # Standard Warning Amber
    )

default() classmethod

Return the default color schema.

Source code in ngwidgets/color_schema.py
71
72
73
74
75
76
@classmethod
def default(cls):
    """
    Return the default color schema.
    """
    return cls()

display()

Display all available color schemas visually in the UI.

Source code in ngwidgets/color_schema.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def display(self):
    """
    Display all available color schemas visually in the UI.
    """
    for schema in ColorSchema.get_schemas():
        style = (
            "color: white;"
            "width: 75px; "
            "height: 50px; "
            "border: 1px solid #000; "
            "display: flex; "
            "justify-content: center; "
            "align-items: center; "
            "border-radius: 5px;"
        )
        with ui.row().style("margin-bottom: 10px;"):
            ui.label(schema.name).style(style + "background:grey;")
            schema._display_color("Primary", schema.primary, style)
            schema._display_color("Secondary", schema.secondary, style)
            schema._display_color("Accent", schema.accent, style)
            schema._display_color("Dark", schema.dark, style)
            schema._display_color("Positive", schema.positive, style)
            schema._display_color("Negative", schema.negative, style)
            schema._display_color("Info", schema.info, style)
            schema._display_color("Warning", schema.warning, style)

get_schemas() classmethod

Return a list of all available color schemas.

Source code in ngwidgets/color_schema.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@classmethod
def get_schemas(cls):
    """
    Return a list of all available color schemas.
    """
    return [
        cls.default(),
        cls.indigo(),
        cls.red(),
        cls.pink_red(),
        cls.purple(),
        cls.deep_purple(),
        cls.blue(),
        cls.light_blue(),
        cls.cyan(),
        cls.teal(),
        cls.green(),
        cls.light_green(),
        cls.lime(),
        cls.yellow(),
        cls.amber(),
        cls.orange(),
        cls.deep_orange(),
        cls.brown(),
        cls.grey(),
        cls.blue_grey(),
    ]

green() classmethod

Return a color schema for the Green color palette from Material Palette. see https://www.materialpalette.com/green/green

Source code in ngwidgets/color_schema.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
@classmethod
def green(cls):
    """
    Return a color schema for the Green color palette from Material Palette.
    see https://www.materialpalette.com/green/green
    """
    return cls(
        name="green",
        primary="#4CAF50",
        secondary="#C8E6C9",
        accent="#388E3C",
        dark="#1B5E20",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

grey() classmethod

Return a color schema for the Grey color palette from Material Palette. see https://www.materialpalette.com/grey/grey

Source code in ngwidgets/color_schema.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
@classmethod
def grey(cls):
    """
    Return a color schema for the Grey color palette from Material Palette.
    see https://www.materialpalette.com/grey/grey
    """
    return cls(
        name="grey",
        primary="#9E9E9E",
        secondary="#F5F5F5",
        accent="#616161",
        dark="#212121",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

indigo() classmethod

Return a color schema for the Indigo color palette from Material Palette. see https://www.materialpalette.com/indigo/indigo

Source code in ngwidgets/color_schema.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
@classmethod
def indigo(cls):
    """
    Return a color schema for the Indigo color palette from Material Palette.
    see https://www.materialpalette.com/indigo/indigo
    """
    color_schema = cls(
        name="indigo",
        primary="#3F51B5",
        secondary="#5C6BC0",
        accent="#8A72AC",
        dark="#1A237E",
        positive="#28A745",
        negative="#D32F2F",
        info="#536DFE",
        warning="#FFB74D",
    )
    return color_schema

light_blue() classmethod

Return a color schema for the Light Blue color palette from Material Palette. see https://www.materialpalette.com/blue/light-blue

Source code in ngwidgets/color_schema.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
@classmethod
def light_blue(cls):
    """
    Return a color schema for the Light Blue color palette from Material Palette.
    see https://www.materialpalette.com/blue/light-blue
    """
    return cls(
        name="light_blue",
        primary="#03A9F4",  # Light Blue
        secondary="#81D4FA",  # Lightest Blue
        accent="#0288D1",  # Dark Light Blue
        dark="#01579B",  # Deepest Light Blue
        positive="#4CAF50",  # Standard Positive Green
        negative="#D32F2F",  # Standard Negative Red
        info="#2196F3",  # Standard Info Blue
        warning="#FFC107",  # Standard Warning Amber
    )

light_green() classmethod

Return a color schema for the Light Green color palette from Material Palette. see https://www.materialpalette.com/green/light-green

Source code in ngwidgets/color_schema.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
@classmethod
def light_green(cls):
    """
    Return a color schema for the Light Green color palette from Material Palette.
    see https://www.materialpalette.com/green/light-green
    """
    return cls(
        name="light_green",
        primary="#8BC34A",  # Light Green
        secondary="#DCEDC8",  # Lightest Green
        accent="#689F38",  # Dark Light Green
        dark="#33691E",  # Deepest Light Green
        positive="#4CAF50",  # Standard Positive Green
        negative="#D32F2F",  # Standard Negative Red
        info="#2196F3",  # Standard Info Blue
        warning="#FFC107",  # Standard Warning Amber
    )

lime() classmethod

Return a color schema for the Lime color palette from Material Palette. see https://www.materialpalette.com/lime/lime

Source code in ngwidgets/color_schema.py
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
@classmethod
def lime(cls):
    """
    Return a color schema for the Lime color palette from Material Palette.
    see https://www.materialpalette.com/lime/lime
    """
    return cls(
        name="lime",
        primary="#CDDC39",
        secondary="#F0F4C3",
        accent="#AFB42B",
        dark="#827717",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

orange() classmethod

Return a color schema for the Orange color palette from Material Palette. see https://www.materialpalette.com/orange/orange

Source code in ngwidgets/color_schema.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
@classmethod
def orange(cls):
    """
    Return a color schema for the Orange color palette from Material Palette.
    see https://www.materialpalette.com/orange/orange
    """
    return cls(
        name="orange",
        primary="#FF9800",
        secondary="#FFE0B2",
        accent="#FF5722",
        dark="#E64A19",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

pink_red() classmethod

Return a color schema for the Pink/Red color palette from Material Palette. see https://www.materialpalette.com/pink/red

Source code in ngwidgets/color_schema.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
@classmethod
def pink_red(cls):
    """
    Return a color schema for the Pink/Red color palette from Material Palette.
    see https://www.materialpalette.com/pink/red
    """
    return cls(
        name="pink_red",
        primary="#E91E63",  # Pink
        secondary="#F8BBD0",  # Light Pink
        accent="#C2185B",  # Dark Pink
        dark="#880E4F",  # Deepest Pink
        positive="#4CAF50",  # Standard Positive Green
        negative="#D32F2F",  # Standard Negative Red
        info="#2196F3",  # Standard Info Blue
        warning="#FFC107",  # Standard Warning Amber
    )

purple() classmethod

Return a color schema for the Purple color palette from Material Palette. see https://www.materialpalette.com/purple/purple

Source code in ngwidgets/color_schema.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
@classmethod
def purple(cls):
    """
    Return a color schema for the Purple color palette from Material Palette.
    see https://www.materialpalette.com/purple/purple
    """
    return cls(
        name="purple",
        primary="#9C27B0",  # Purple
        secondary="#CE93D8",  # Light Purple
        accent="#7B1FA2",  # Dark Purple
        dark="#4A148C",  # Deepest Purple
        positive="#4CAF50",  # Standard Positive Green
        negative="#D32F2F",  # Standard Negative Red
        info="#2196F3",  # Standard Info Blue
        warning="#FFC107",  # Standard Warning Amber
    )

red() classmethod

Return a color schema for the Red color palette from Material Palette. see https://www.materialpalette.com/red/red

Source code in ngwidgets/color_schema.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
@classmethod
def red(cls):
    """
    Return a color schema for the Red color palette from Material Palette.
    see https://www.materialpalette.com/red/red
    """
    return cls(
        name="red",
        primary="#F44336",
        secondary="#FFCDD2",
        accent="#D32F2F",
        dark="#B71C1C",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

teal() classmethod

Return a color schema for the Teal color palette from Material Palette. see https://www.materialpalette.com/teal/teal

Source code in ngwidgets/color_schema.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
@classmethod
def teal(cls):
    """
    Return a color schema for the Teal color palette from Material Palette.
    see https://www.materialpalette.com/teal/teal
    """
    return cls(
        name="teal",
        primary="#009688",
        secondary="#80CBC4",
        accent="#00796B",
        dark="#004D40",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

yellow() classmethod

Return a color schema for the Yellow color palette from Material Palette. see https://www.materialpalette.com/yellow/yellow

Source code in ngwidgets/color_schema.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
@classmethod
def yellow(cls):
    """
    Return a color schema for the Yellow color palette from Material Palette.
    see https://www.materialpalette.com/yellow/yellow
    """
    return cls(
        name="yellow",
        primary="#FFEB3B",
        secondary="#FFF9C4",
        accent="#FBC02D",
        dark="#F57F17",
        positive="#4CAF50",
        negative="#D32F2F",
        info="#2196F3",
        warning="#FFC107",
    )

combobox

Created on 2024-07-04

@author: wf

ComboBox

A ComboBox class that encapsulates a UI selection control which allows both drop-down and free-form text input, suitable for dynamic user interfaces.

Attributes:

Name Type Description
label_base str

The base text for the combobox label.

options Iterable[str]

The current list of options available in the combobox.

select Select

The UI component instance, allowing for both selection and direct input.

Source code in ngwidgets/combobox.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
class ComboBox:
    """
    A ComboBox class that encapsulates a UI selection control which allows both drop-down and free-form text input,
    suitable for dynamic user interfaces.

    Attributes:
        label_base (str): The base text for the combobox label.
        options (Iterable[str]): The current list of options available in the combobox.
        select (ui.Select): The UI component instance, allowing for both selection and direct input.
    """

    def __init__(
        self, label: str, options: Iterable[str], width_chars: int = 40, **kwargs
    ):
        self.label_base = label
        self.width_chars = width_chars
        self.options = self.prepare_options(options)
        self.select = None
        self.kwargs = kwargs
        self.setup_ui()

    def prepare_options(self, options: Union[Iterable[str], Dict[str, str]]):
        if isinstance(options, dict):
            return options  # Use directly as dict supports 'items' which include both keys and values
        if not isinstance(options, list):
            options = list(options)
        if all(options):
            options = sorted(options)
        return options  # Fallback if options is neither iterable nor dict

    def setup_ui(self):
        """Initializes the UI component for the combobox with optional text input capability."""
        self.select = ui.select(
            label=f"{self.label_base} ({len(self.options)})",
            options=self.options,
            with_input=True,  # Allows users to either select from the dropdown or enter custom text.
            **self.kwargs,  # Pass all additional keyword arguments to the select component.
        )
        self.select.style(f"width: {self.width_chars}ch")  #

    def update_options(
        self, new_options: Iterable[str], limit: int = None, options_count: int = None
    ):
        """Updates the options available in the combobox and refreshes the label, applying a limit to the number of items displayed if specified,
        and showing total available options if different from displayed due to the limit.

        Args:
            new_options (Interable[str]): The new options to update in the combobox.
            limit (int, optional): Maximum number of options to display. If None, all options are displayed.
            options_count (int, optional): The total count of available options, relevant only if a limit is set.
        """
        new_options = self.prepare_options(new_options)

        # Apply limit if specified
        if (
            limit is not None
            and isinstance(new_options, list)
            and len(new_options) > limit
        ):
            new_options = new_options[:limit]
            # Use options_count to show how many are available in total
            total_options = (
                options_count if options_count is not None else len(new_options)
            )
            label_text = f"{self.label_base} ({len(new_options)}/{total_options})"
        else:
            label_text = f"{self.label_base} ({len(new_options)})"

        self.options = new_options
        self.select.options = self.options
        self.select._props["label"] = label_text = label_text

        # Explicitly update the UI to reflect changes
        self.select.update()

setup_ui()

Initializes the UI component for the combobox with optional text input capability.

Source code in ngwidgets/combobox.py
45
46
47
48
49
50
51
52
53
def setup_ui(self):
    """Initializes the UI component for the combobox with optional text input capability."""
    self.select = ui.select(
        label=f"{self.label_base} ({len(self.options)})",
        options=self.options,
        with_input=True,  # Allows users to either select from the dropdown or enter custom text.
        **self.kwargs,  # Pass all additional keyword arguments to the select component.
    )
    self.select.style(f"width: {self.width_chars}ch")  #

update_options(new_options, limit=None, options_count=None)

Updates the options available in the combobox and refreshes the label, applying a limit to the number of items displayed if specified, and showing total available options if different from displayed due to the limit.

Parameters:

Name Type Description Default
new_options Interable[str]

The new options to update in the combobox.

required
limit int

Maximum number of options to display. If None, all options are displayed.

None
options_count int

The total count of available options, relevant only if a limit is set.

None
Source code in ngwidgets/combobox.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def update_options(
    self, new_options: Iterable[str], limit: int = None, options_count: int = None
):
    """Updates the options available in the combobox and refreshes the label, applying a limit to the number of items displayed if specified,
    and showing total available options if different from displayed due to the limit.

    Args:
        new_options (Interable[str]): The new options to update in the combobox.
        limit (int, optional): Maximum number of options to display. If None, all options are displayed.
        options_count (int, optional): The total count of available options, relevant only if a limit is set.
    """
    new_options = self.prepare_options(new_options)

    # Apply limit if specified
    if (
        limit is not None
        and isinstance(new_options, list)
        and len(new_options) > limit
    ):
        new_options = new_options[:limit]
        # Use options_count to show how many are available in total
        total_options = (
            options_count if options_count is not None else len(new_options)
        )
        label_text = f"{self.label_base} ({len(new_options)}/{total_options})"
    else:
        label_text = f"{self.label_base} ({len(new_options)})"

    self.options = new_options
    self.select.options = self.options
    self.select._props["label"] = label_text = label_text

    # Explicitly update the UI to reflect changes
    self.select.update()

components

Created on 2023-12-16

This components module has the classes Component and Components for managing components of an online software components bazaar It was created for nicegui components but may be adapted to other context by modifying the topic,

Prompts for LLM: - Create Python classes Component and Components for managing UI components, including loading and saving functionality. - Develop a data class in Python to represent a UI component with the attributes: name: The title or identifier of the component. source: A web link directing to where the component's code can be found. demo_url: A web link to an image or video showing the component in action. doc_url: A web link to any documentation or detailed information about the component. issue: Reference to any known issues or bugs related to the component, typically tracked on platforms like GitHub. fixed: Date marking when any known issues or bugs with the component were resolved. - Implement methods in Components to load and save a collection of Component instances from/to a YAML file.

Main author: OpenAI's language model (instructed by WF)

Component

Represents a single component with its associated metadata.

Attributes:

Name Type Description
name str

The name of the component.

description(str) str

a multiline description of the component

source Optional[str]

The source code URL of the component, if available.

demo_url Optional[str]

The URL of an online demo of the component, if available.

demo_image_url Optional[str]

The URL of a picture and/or video demonstrating the component, if available.

doc_url Optional[str]

The URL of the documentation for the component, if available.

issue Optional[str]

The identifier for any related issue (github), if applicable.

fixed Optional[str]

The date on which any related issue was fixed, if applicable.

Source code in ngwidgets/components.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@lod_storable
class Component:
    """
    Represents a single component with its associated metadata.

    Attributes:
        name (str): The name of the component.
        description(str): a multiline description of the component
        source (Optional[str]): The source code URL of the component, if available.
        demo_url (Optional[str]): The URL of an online demo of the component, if available.
        demo_image_url (Optional[str]): The URL of a picture and/or video demonstrating the component, if available.
        doc_url (Optional[str]): The URL of the documentation for the component, if available.
        issue (Optional[str]): The identifier for any related issue (github), if applicable.
        fixed (Optional[str]): The date on which any related issue was fixed, if applicable.
    """

    name: str
    description: Optional[str] = None
    source: Optional[str] = None
    demo_url: Optional[str] = None
    demo_image_url: Optional[str] = None
    doc_url: Optional[str] = None
    issue: Optional[int] = None
    fixed: Optional[str] = None

Components

Components

Source code in ngwidgets/components.py
55
56
57
58
59
60
61
62
@lod_storable
class Components:
    """
    Components
    """

    version: Optional[str] = None
    components: List[Component] = field(default_factory=list)

components_view

Created on 2023-12-16

@author: wf

ComponentView

Display a single component

Source code in ngwidgets/components_view.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class ComponentView:
    """
    Display a single component
    """

    def __init__(self, project: Project, component: Component):
        self.project = project
        self.component = component

    def setup(self, container) -> ui.card:
        """
        Setup a card for the component
        """
        with container:
            self.card = ui.card()
            with self.card:
                with ui.row().classes("flex w-full items-center"):
                    # Title
                    title = f"{self.component.name}"
                    ui.label(title).classes("text-2xl")
                    html_markup = ""
                    delim = ""
                    if self.component.demo_url:
                        link = Link.create(self.component.demo_url, "demo")
                        html_markup += link
                        delim = " "
                    if self.component.source:
                        url = self.project.components_url.replace(
                            "/.components.yaml", self.component.source
                        )
                        link = Link.create(url, self.component.name)
                        html_markup += delim + link
                        delim = " "
                    if self.component.issue:
                        url = f"{self.project.github}/issues/{self.component.issue}"
                        link = Link.create(url, f"#{self.component.issue}")
                        html_markup += delim + link
                        delim = " "
                    ui.html(html_markup)
                    if self.component.demo_image_url:
                        ui.image(self.component.demo_image_url)
                    if self.component.description:
                        ui.label(self.component.description)

setup(container)

Setup a card for the component

Source code in ngwidgets/components_view.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def setup(self, container) -> ui.card:
    """
    Setup a card for the component
    """
    with container:
        self.card = ui.card()
        with self.card:
            with ui.row().classes("flex w-full items-center"):
                # Title
                title = f"{self.component.name}"
                ui.label(title).classes("text-2xl")
                html_markup = ""
                delim = ""
                if self.component.demo_url:
                    link = Link.create(self.component.demo_url, "demo")
                    html_markup += link
                    delim = " "
                if self.component.source:
                    url = self.project.components_url.replace(
                        "/.components.yaml", self.component.source
                    )
                    link = Link.create(url, self.component.name)
                    html_markup += delim + link
                    delim = " "
                if self.component.issue:
                    url = f"{self.project.github}/issues/{self.component.issue}"
                    link = Link.create(url, f"#{self.component.issue}")
                    html_markup += delim + link
                    delim = " "
                ui.html(html_markup)
                if self.component.demo_image_url:
                    ui.image(self.component.demo_image_url)
                if self.component.description:
                    ui.label(self.component.description)

ComponentsView

Display a collection of components in a grid layout

Source code in ngwidgets/components_view.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ComponentsView:
    """
    Display a collection of components in a grid layout
    """

    def __init__(
        self, webserver: "InputWebserver", projects: Projects, project: Project
    ):
        self.webserver = webserver
        self.projects = projects
        self.project = project
        self.components = project.get_components(projects.default_directory)
        self.displayed_components = []
        self.container = None
        self.slider = None
        self.page_size = 8

    def setup(self):
        """
        Set up the UI elements to render the collection of components
        as a grid layout with four columns.
        """
        self.project_container = ui.grid(columns=4)
        self.project_view = ProjectView(self.project)
        self.project_view.setup(self.project_container)
        if self.components:
            self.slider = ui.slider(
                min=1,
                max=len(self.components.components) // self.page_size + 1,
                step=1,
                value=1,
                on_change=self.update_display,
            )
            self.container = ui.grid(columns=4)
        self.update_display()

    def update_display(self, *_args):
        """
        Update the displayed components based on the slider's position
        """
        if not self.components:
            return
        start_index = (self.slider.value - 1) * self.page_size
        end_index = start_index + self.page_size
        displayed_components = self.components.components[start_index:end_index]

        # Clear existing components in the container
        self.container.clear()

        # Add new components to the container
        with self.container:
            for component in displayed_components:
                cv = ComponentView(self.project, component)
                cv.setup(self.container)

setup()

Set up the UI elements to render the collection of components as a grid layout with four columns.

Source code in ngwidgets/components_view.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def setup(self):
    """
    Set up the UI elements to render the collection of components
    as a grid layout with four columns.
    """
    self.project_container = ui.grid(columns=4)
    self.project_view = ProjectView(self.project)
    self.project_view.setup(self.project_container)
    if self.components:
        self.slider = ui.slider(
            min=1,
            max=len(self.components.components) // self.page_size + 1,
            step=1,
            value=1,
            on_change=self.update_display,
        )
        self.container = ui.grid(columns=4)
    self.update_display()

update_display(*_args)

Update the displayed components based on the slider's position

Source code in ngwidgets/components_view.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def update_display(self, *_args):
    """
    Update the displayed components based on the slider's position
    """
    if not self.components:
        return
    start_index = (self.slider.value - 1) * self.page_size
    end_index = start_index + self.page_size
    displayed_components = self.components.components[start_index:end_index]

    # Clear existing components in the container
    self.container.clear()

    # Add new components to the container
    with self.container:
        for component in displayed_components:
            cv = ComponentView(self.project, component)
            cv.setup(self.container)

dateparser

Created on 2023-12-03

@author: wf

DateParser

A parser for converting date strings with timezone information into ISO 8601 format.

Attributes:

Name Type Description
aliases list

A list of tuples mapping timezone string aliases to their canonical form.

whois_timezone_info dict

A dictionary mapping timezone abbreviations to their UTC offsets.

Source code in ngwidgets/dateparser.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
class DateParser:
    """A parser for converting date strings with timezone information into ISO 8601 format.

    Attributes:
        aliases (list): A list of tuples mapping timezone string aliases to their canonical form.
        whois_timezone_info (dict): A dictionary mapping timezone abbreviations to their UTC offsets.
    """

    def __init__(self):
        # https://stackoverflow.com/a/54629675/1497139
        self.aliases = [
            ('"GMT"', "(GMT)"),
            ("(WET DST)", "(WEST)"),
            ("+0200 (MET DST)", "+0200"),
            ("+0200 (METDST)", "+0200"),
            (" METDST", " +0200"),
            (" MET DST", " +0200"),
            ("(GMT)", "+0000"),
            ("+0100 (GMT Daylight Time)", "+0100"),
            ("+0100 (Etc/GMT)", "-0100"),
            ("Etc/GMT", "-0000"),
            (" pst", " PST"),  # Convert lowercase 'pst' to uppercase 'PST'
            (" est", " EST"),
            ("(MSK/MSD)", "(MSK)"),
            ("(GMT Standard Time)", "(GMT)"),
            ("(Mountain Daylight Time)", "(MDT)"),
            (" Eastern Daylight Time", "-0800 (EDT)"),
            ("(Eastern Standard Time)", "(EST)"),
            ("(Eastern Daylight Time)", "(EDT)"),
            ("(Pacific Daylight Time)", "(PDT)"),
            ("(Eastern Standard Time)", "(EST)"),
        ]
        self.regexp_aliases = [
            # remove superfluous (added by ...)
            (r"\(added by [^\)]+\)", ""),
            # Regular expression to remove conflicting timezone information like (GMT-1)
            # but only if it follows a standard timezone offset like +0100
            # example +0100 (GMT-1)
            (r"(\+\d{4}|\-\d{4}) \(GMT[+-]\d+\)", r"\1"),
            # Regular expression to correct conflicting timezone information like +-0100
            # +-0100
            (r"\+\-(\d{4})", r"-\1"),  # Convert +-0100 to -0100
            # Regular expression to correct timezone information like +-800
            (r"\+\-(\d{3})", r"-0\1"),  # Convert +-800 to -0800
        ]
        # Add generic aliases for a range of timezones
        for hour in range(-12, 15):  # Ranges from GMT-12 to GMT+14
            sign = "+" if hour >= 0 else "-"
            hour_abs = abs(hour)

            # Example: ("(GMT+00:00)","+0000")
            self.aliases.append(
                (f"(GMT{sign}{hour_abs:02d}:00)", f"{sign}{hour_abs:02d}00")
            )
            # Example: ("(GMT-1)","-0100"),
            self.aliases.append((f"(GMT{sign}{hour})", f"{sign}0{hour_abs}00"))

            # Handling Etc/GMT formats
            # Example: ("Etc/GMT+1", "+0100")
            gmt_sign = "" if hour <= 0 else "+"
            self.aliases.append((f"Etc/GMT{gmt_sign}{hour}", f"{sign}{hour_abs:02d}00"))

        self.timezone_hours = {
            "AoE": {"offset": -12, "description": "Anywhere on Earth"},
            "Y": {"offset": -12, "description": "Yankee Time Zone"},
            "NUT": {"offset": -11, "description": "Niue Time"},
            "SST": {"offset": -11, "description": "Samoa Standard Time"},
            "X": {"offset": -11, "description": "X-ray Time Zone"},
            "CKT": {"offset": -10, "description": "Cook Island Time"},
            "HST": {"offset": -10, "description": "Hawaii Standard Time"},
            "TAHT": {"offset": -10, "description": "Tahiti Time"},
            "W": {"offset": -10, "description": "Whiskey Time Zone"},
            "AKST": {"offset": -9, "description": "Alaska Standard Time"},
            "GAMT": {"offset": -9, "description": "Gambier Time"},
            "HDT": {"offset": -9, "description": "Hawaii-Aleutian Daylight Time"},
            "V": {"offset": -9, "description": "Victor Time Zone"},
            "AKDT": {"offset": -8, "description": "Alaska Daylight Time"},
            "PST": {"offset": -8, "description": "Pacific Standard Time"},
            "PT": {"offset": -8, "description": "Pacific Time"},
            "U": {"offset": -8, "description": "Uniform Time Zone"},
            "MST": {"offset": -7, "description": "Mountain Standard Time"},
            "MT": {"offset": -7, "description": "Mountain Time"},
            "PDT": {"offset": -7, "description": "Pacific Daylight Time"},
            "T": {"offset": -7, "description": "Tango Time Zone"},
            "CST": {"offset": -6, "description": "Central Standard Time"},
            "CT": {"offset": -6, "description": "Central Time"},
            "EAST": {"offset": -6, "description": "Easter Island Standard Time"},
            "GALT": {"offset": -6, "description": "Galapagos Time"},
            "MDT": {"offset": -6, "description": "Mountain Daylight Time"},
            "S": {"offset": -6, "description": "Sierra Time Zone"},
            "ACT": {"offset": -5, "description": "Acre Time"},
            "CDT": {"offset": -5, "description": "Central Daylight Time"},
            "CIST": {"offset": -5, "description": "Clipperton Island Standard Time"},
            "COT": {"offset": -5, "description": "Colombia Time"},
            "EASST": {"offset": -5, "description": "Easter Island Summer Time"},
            "ECT": {"offset": -5, "description": "Ecuador Time"},
            "EST": {"offset": -5, "description": "Eastern Standard Time"},
            "ET": {"offset": -5, "description": "Eastern Time"},
            "PET": {"offset": -5, "description": "Peru Time"},
            "R": {"offset": -5, "description": "Romeo Time Zone"},
            "AMT": {"offset": -4, "description": "Amazon Time"},
            "AT": {"offset": -4, "description": "Atlantic Time"},
            "BOT": {"offset": -4, "description": "Bolivia Time"},
            "CIDST": {"offset": -4, "description": "Cambridge Bay Daylight Time"},
            "CLT": {"offset": -4, "description": "Chile Standard Time"},
            "EDT": {"offset": -4, "description": "Eastern Daylight Time"},
            "FKT": {"offset": -4, "description": "Falkland Islands Time"},
            "GYT": {"offset": -4, "description": "Guyana Time"},
            "PYT": {"offset": -4, "description": "Paraguay Time"},
            "Q": {"offset": -4, "description": "Quebec Time Zone"},
            "VET": {"offset": -4, "description": "Venezuelan Standard Time"},
            "AMST": {"offset": -3, "description": "Amazon Summer Time"},
            "ART": {"offset": -3, "description": "Argentina Time"},
            "BRT": {"offset": -3, "description": "Brasilia Time"},
            "CLST": {"offset": -3, "description": "Chile Summer Time"},
            "FKST": {"offset": -3, "description": "Falkland Islands Summer Time"},
            "GFT": {"offset": -3, "description": "French Guiana Time"},
            "P": {"offset": -3, "description": "Papa Time Zone"},
            "PMST": {"offset": -3, "description": "Pierre & Miquelon Standard Time"},
            "PYST": {"offset": -3, "description": "Paraguay Summer Time"},
            "ROTT": {"offset": -3, "description": "Rothera Time"},
            "SRT": {"offset": -3, "description": "Suriname Time"},
            "UYT": {"offset": -3, "description": "Uruguay Time"},
            "WARST": {"offset": -3, "description": "Western Argentina Summer Time"},
            "WGT": {"offset": -3, "description": "West Greenland Time"},
            "BRST": {"offset": -2, "description": "Brasilia Summer Time"},
            "FNT": {"offset": -2, "description": "Fernando de Noronha Time"},
            "O": {"offset": -2, "description": "Oscar Time Zone"},
            "PMDT": {"offset": -2, "description": "Pierre & Miquelon Daylight Time"},
            "UYST": {"offset": -2, "description": "Uruguay Summer Time"},
            "WGST": {"offset": -2, "description": "West Greenland Summer Time"},
            "AZOT": {"offset": -1, "description": "Azores Standard Time"},
            "CVT": {"offset": -1, "description": "Cape Verde Time"},
            "EGT": {"offset": -1, "description": "Eastern Greenland Time"},
            "N": {"offset": -1, "description": "November Time Zone"},
            "AZOST": {"offset": 0, "description": "Azores Summer Time"},
            "EGST": {"offset": 0, "description": "Eastern Greenland Summer Time"},
            "GMT": {"offset": 0, "description": "Greenwich Mean Time"},
            "UT": {"offset": 0, "description": "Universal Time"},
            "UTC": {"offset": 0, "description": "Coordinated Universal Time"},
            "WET": {"offset": 0, "description": "Western European Time"},
            "WT": {"offset": 0, "description": "Western Sahara Standard Time"},
            "Z": {"offset": 0, "description": "Zulu Time Zone"},
            "A": {"offset": 1, "description": "Alpha Time Zone"},
            "CET": {"offset": 1, "description": "Central European Time"},
            "MET": {"offset": 1, "description": "Middle European Time"},
            "MEZ": {"offset": 1, "description": "Middle European Time"},
            "WAT": {"offset": 1, "description": "West Africa Time"},
            "WEST": {"offset": 1, "description": "Western European Summer Time"},
            "B": {"offset": 2, "description": "Bravo Time Zone"},
            "CAT": {"offset": 2, "description": "Central Africa Time"},
            "CEDT": {"offset": 2, "description": "Central European Daylight Time"},
            "CES": {"offset": 2, "description": "Central European Summer Time"},
            "CEST": {"offset": 2, "description": "Central European Summer Time"},
            "EET": {"offset": 2, "description": "Eastern European Time"},
            "MES": {"offset": 2, "description": "Middle European Summer Time"},
            "MEST": {"offset": 2, "description": "Middle European Summer Time"},
            "MESZ": {"offset": 2, "description": "Middle European Summer Time"},
            "METDST": {
                "offset": 2,
                "description": "Middle European Time Daylight Saving Time",
            },
            "MET DST": {
                "offset": 2,
                "description": "Middle European Time Daylight Saving Time",
            },
            "SAST": {"offset": 2, "description": "South Africa Standard Time"},
            "WAST": {"offset": 2, "description": "West Africa Summer Time"},
            "NDT": {"offset": 2.5, "description": "Newfoundland Daylight Time"},
            "AST": {"offset": 3, "description": "Arabia Standard Time"},
            "C": {"offset": 3, "description": "Charlie Time Zone"},
            "EAT": {"offset": 3, "description": "East Africa Time"},
            "EEST": {"offset": 3, "description": "Eastern European Summer Time"},
            "FET": {"offset": 3, "description": "Further-Eastern European Time"},
            "IDT": {"offset": 3, "description": "Israel Daylight Time"},
            "MSK": {"offset": 3, "description": "Moscow Time"},
            "SYOT": {"offset": 3, "description": "Syowa Time"},
            "TRT": {"offset": 3, "description": "Turkey Time"},
            "IRST": {"offset": 3.5, "description": "Iran Standard Time"},
            "NST": {"offset": 3.5, "description": "Newfoundland Standard Time"},
            "ADT": {"offset": 4, "description": "Atlantic Daylight Time"},
            "AZT": {"offset": 4, "description": "Azerbaijan Time"},
            "D": {"offset": 4, "description": "Delta Time Zone"},
            "GET": {"offset": 4, "description": "Georgia Standard Time"},
            "GST": {"offset": 4, "description": "Gulf Standard Time"},
            "KUYT": {"offset": 4, "description": "Kuybyshev Time"},
            "MSD": {"offset": 4, "description": "Moscow Daylight Time"},
            "MUT": {"offset": 4, "description": "Mauritius Time"},
            "RET": {"offset": 4, "description": "Réunion Time"},
            "SAMT": {"offset": 4, "description": "Samara Time"},
            "SCT": {"offset": 4, "description": "Seychelles Time"},
            "AFT": {"offset": 4.5, "description": "Afghanistan Time"},
            "IRDT": {"offset": 4.5, "description": "Iran Daylight Time"},
            "AQTT": {"offset": 5, "description": "Aqtobe Time"},
            "AZST": {"offset": 5, "description": "Azerbaijan Summer Time"},
            "E": {"offset": 5, "description": "Echo Time Zone"},
            "MAWT": {"offset": 5, "description": "Mawson Station Time"},
            "MVT": {"offset": 5, "description": "Maldives Time"},
            "ORAT": {"offset": 5, "description": "Oral Time"},
            "PKT": {"offset": 5, "description": "Pakistan Standard Time"},
            "TFT": {"offset": 5, "description": "French Southern and Antarctic Time"},
            "TJT": {"offset": 5, "description": "Tajikistan Time"},
            "TMT": {"offset": 5, "description": "Turkmenistan Time"},
            "UZT": {"offset": 5, "description": "Uzbekistan Time"},
            "YEKT": {"offset": 5, "description": "Yekaterinburg Time"},
            "IST": {"offset": 5.5, "description": "Indian Standard Time"},
            "NPT": {"offset": 5.5, "description": "Nepal Time"},
            "ALMT": {"offset": 6, "description": "Alma-Ata Time"},
            "BST": {"offset": 6, "description": "Bangladesh Standard Time"},
            "BTT": {"offset": 6, "description": "Bhutan Time"},
            "F": {"offset": 6, "description": "Foxtrot Time Zone"},
            "IOT": {"offset": 6, "description": "Indian Ocean Time"},
            "KGT": {"offset": 6, "description": "Kyrgyzstan Time"},
            "OMST": {"offset": 6, "description": "Omsk Time"},
            "QYZT": {"offset": 6, "description": "Qyzylorda Time"},
            "VOST": {"offset": 6, "description": "Vostok Station Time"},
            "YEKST": {"offset": 6, "description": "Yekaterinburg Summer Time"},
            "CCT": {"offset": 6.5, "description": "Cocos Islands Time"},
            "MMT": {"offset": 6.5, "description": "Myanmar Time"},
            "CXT": {"offset": 7, "description": "Christmas Island Time"},
            "DAVT": {"offset": 7, "description": "Davis Time"},
            "G": {"offset": 7, "description": "Golf Time Zone"},
            "HOVT": {"offset": 7, "description": "Hovd Time"},
            "ICT": {"offset": 7, "description": "Indochina Time"},
            "KRAT": {"offset": 7, "description": "Krasnoyarsk Time"},
            "NOVST": {"offset": 7, "description": "Novosibirsk Summer Time"},
            "NOVT": {"offset": 7, "description": "Novosibirsk Time"},
            "OMSST": {"offset": 7, "description": "Omsk Summer Time"},
            "WIB": {"offset": 7, "description": "Western Indonesia Time"},
            "AWST": {"offset": 8, "description": "Australian Western Standard Time"},
            "BNT": {"offset": 8, "description": "Brunei Time"},
            "CAST": {"offset": 8, "description": "Casey Time"},
            "CHOT": {"offset": 8, "description": "Choibalsan Time"},
            "H": {"offset": 8, "description": "Hotel Time Zone"},
            "HKT": {"offset": 8, "description": "Hong Kong Time"},
            "HOVST": {"offset": 8, "description": "Hovd Summer Time"},
            "IRKT": {"offset": 8, "description": "Irkutsk Time"},
            "KRAST": {"offset": 8, "description": "Krasnoyarsk Summer Time"},
            "MYT": {"offset": 8, "description": "Malaysia Time"},
            "PHT": {"offset": 8, "description": "Philippine Time"},
            "SGT": {"offset": 8, "description": "Singapore Time"},
            "ULAT": {"offset": 8, "description": "Ulaanbaatar Time"},
            "WITA": {"offset": 8, "description": "Central Indonesia Time"},
            "ACWST": {
                "offset": 8.75,
                "description": "Australian Central Western Standard Time",
            },
            "AWDT": {"offset": 9, "description": "Australian Western Daylight Time"},
            "CHOST": {"offset": 9, "description": "Choibalsan Summer Time"},
            "I": {"offset": 9, "description": "India Time Zone"},
            "IRKST": {"offset": 9, "description": "Irkutsk Summer Time"},
            "JST": {"offset": 9, "description": "Japan Standard Time"},
            "KST": {"offset": 9, "description": "Korea Standard Time"},
            "PWT": {"offset": 9, "description": "Palau Time"},
            "TLT": {"offset": 9, "description": "Timor Leste Time"},
            "ULAST": {"offset": 9, "description": "Ulaanbaatar Summer Time"},
            "WIT": {"offset": 9, "description": "Eastern Indonesia Time"},
            "YAKT": {"offset": 9, "description": "Yakutsk Time"},
            "ACST": {"offset": 9.5, "description": "Australian Central Standard Time"},
            "MART": {"offset": 9.5, "description": "Marquesas Time"},
            "AEST": {"offset": 10, "description": "Australian Eastern Standard Time"},
            "AET": {"offset": 10, "description": "Australian Eastern Time"},
            "CHUT": {"offset": 10, "description": "Chuuk Time"},
            "ChST": {"offset": 10, "description": "Chamorro Standard Time"},
            "DDUT": {"offset": 10, "description": "Dumont d'Urville Time"},
            "K": {"offset": 10, "description": "Kilo Time Zone"},
            "PGT": {"offset": 10, "description": "Papua New Guinea Time"},
            "VLAT": {"offset": 10, "description": "Vladivostok Time"},
            "YAKST": {"offset": 10, "description": "Yakutsk Summer Time"},
            "YAPT": {"offset": 10, "description": "Yap Time"},
            # Continuing from the previous part...
            "ACDT": {"offset": 10.5, "description": "Australian Central Daylight Time"},
            "LHST": {"offset": 10.5, "description": "Lord Howe Standard Time"},
            "AEDT": {"offset": 11, "description": "Australian Eastern Daylight Time"},
            "KOST": {"offset": 11, "description": "Kosrae Time"},
            "L": {"offset": 11, "description": "Lima Time Zone"},
            "LHDT": {"offset": 11, "description": "Lord Howe Daylight Time"},
            "MAGT": {"offset": 11, "description": "Magadan Time"},
            "NCT": {"offset": 11, "description": "New Caledonia Time"},
            "NFT": {"offset": 11, "description": "Norfolk Time"},
            "PONT": {"offset": 11, "description": "Pohnpei Time"},
            "SAKT": {"offset": 11, "description": "Sakhalin Time"},
            "SBT": {"offset": 11, "description": "Solomon Islands Time"},
            "SRET": {"offset": 11, "description": "Srednekolymsk Time"},
            "VLAST": {"offset": 11, "description": "Vladivostok Summer Time"},
            "VUT": {"offset": 11, "description": "Vanuatu Time"},
            "ANAST": {"offset": 12, "description": "Anadyr Summer Time"},
            "ANAT": {"offset": 12, "description": "Anadyr Time"},
            "FJT": {"offset": 12, "description": "Fiji Time"},
            "GILT": {"offset": 12, "description": "Gilbert Island Time"},
            "M": {"offset": 12, "description": "Mike Time Zone"},
            "MAGST": {"offset": 12, "description": "Magadan Summer Time"},
            "MHT": {"offset": 12, "description": "Marshall Islands Time"},
            "NRT": {"offset": 12, "description": "Nauru Time"},
            "NZST": {"offset": 12, "description": "New Zealand Standard Time"},
            "PETST": {"offset": 12, "description": "Kamchatka Summer Time"},
            "PETT": {"offset": 12, "description": "Kamchatka Time"},
            "TVT": {"offset": 12, "description": "Tuvalu Time"},
            "WAKT": {"offset": 12, "description": "Wake Island Time"},
            "WFT": {"offset": 12, "description": "Wallis and Futuna Time"},
            "CHAST": {"offset": 12.75, "description": "Chatham Standard Time"},
            "FJST": {"offset": 13, "description": "Fiji Summer Time"},
            "NZDT": {"offset": 13, "description": "New Zealand Daylight Time"},
            "PHOT": {"offset": 13, "description": "Phoenix Island Time"},
            "TKT": {"offset": 13, "description": "Tokelau Time"},
            "TOT": {"offset": 13, "description": "Tonga Time"},
            "CHADT": {"offset": 13.75, "description": "Chatham Daylight Time"},
            "LINT": {"offset": 14, "description": "Line Islands Time"},
            "TOST": {"offset": 14, "description": "Tonga Summer Time"},
            "WST": {"offset": 14, "description": "West Samoa Time"},
        }
        # Convert timezone offsets from hours to seconds and create tzinfos dictionary
        self.tzinfos = {}
        for tz, info in self.timezone_hours.items():
            offset_in_seconds = int(info["offset"] * 3600)
            self.tzinfos[tz] = offset_in_seconds

    def parse_date(self, date_str) -> str:
        """
        Parses a date string and converts it to ISO 8601 format.

        Args:
            date_str (str): The date string to be parsed.

        Returns:
            str:  the ISO 8601 date string
        """
        # Apply regex replacements
        for pattern, replacement in self.regexp_aliases:
            date_str = re.sub(pattern, replacement, date_str)

        # Apply simple string replacements
        for alias, replacement in self.aliases:
            date_str = date_str.replace(alias, replacement)

        parsed_date = parser.parse(date_str, tzinfos=self.tzinfos)
        parsed_date_z = parsed_date.astimezone(pytz.utc)
        # Convert to ISO 8601 format
        iso_date_str = parsed_date_z.isoformat()
        iso_date_str_z = iso_date_str.replace("+00:00", "Z")
        return iso_date_str_z

parse_date(date_str)

Parses a date string and converts it to ISO 8601 format.

Parameters:

Name Type Description Default
date_str str

The date string to be parsed.

required

Returns:

Name Type Description
str str

the ISO 8601 date string

Source code in ngwidgets/dateparser.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
def parse_date(self, date_str) -> str:
    """
    Parses a date string and converts it to ISO 8601 format.

    Args:
        date_str (str): The date string to be parsed.

    Returns:
        str:  the ISO 8601 date string
    """
    # Apply regex replacements
    for pattern, replacement in self.regexp_aliases:
        date_str = re.sub(pattern, replacement, date_str)

    # Apply simple string replacements
    for alias, replacement in self.aliases:
        date_str = date_str.replace(alias, replacement)

    parsed_date = parser.parse(date_str, tzinfos=self.tzinfos)
    parsed_date_z = parsed_date.astimezone(pytz.utc)
    # Convert to ISO 8601 format
    iso_date_str = parsed_date_z.isoformat()
    iso_date_str_z = iso_date_str.replace("+00:00", "Z")
    return iso_date_str_z

debouncer

Debouncing module for managing rapid function calls and providing UI feedback. Created on 2024-06-08

Debouncer

A class to manage debouncing of function calls.

This class allows for debouncing function calls which can be either CPU-bound or I/O-bound. It includes optional callbacks that execute at the start and completion of the debounced function.

Source code in ngwidgets/debouncer.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class Debouncer:
    """A class to manage debouncing of function calls.

    This class allows for debouncing function calls which can be either CPU-bound or I/O-bound.
    It includes optional callbacks that execute at the start and completion of the debounced function.
    """

    def __init__(
        self,
        delay: float = 0.330,
        debounce_cpu_bound: bool = False,
        debounce_task_name: str = "Debounce Task",
        debug: bool = False,
    ):
        """
        Initialize the Debouncer with a specific delay.

        Args:
            delay (float): The debouncing delay in seconds. Default is 0.330 seconds.
            debounce_cpu_bound (bool): If True, use CPU-bound execution; otherwise use I/O-bound execution.
            debounce_task_name (str): The name to use for the task.
            debug(bool): if True show debug info
        """
        self.delay = delay
        self.counter = 0
        self.debounce_cpu_bound = debounce_cpu_bound
        self.debounce_task_name = debounce_task_name
        self.debug = debug
        self.task: Optional[asyncio.Task] = None

    def log(self, call_type: str, func, *args, **kwargs):
        """
        log the call
        """
        if self.debug:
            print(f"calling {call_type} #{self.counter}. time")
            print("function:", func.__name__)
            print("args:", args, kwargs)

    async def debounce(
        self,
        func: Callable,
        *args,
        on_start: Optional[Callable[[], Any]] = None,
        on_done: Optional[Callable[[], Any]] = None,
        **kwargs,
    ):
        """
        Debounce the given function call, using either CPU-bound or I/O-bound execution based on the flag.
        Optional callbacks can be specified for execution at the start and end of the function.

        Args:
            func (Callable): The function to be debounced.
            on_start (Optional[Callable[[], Any]]): Function to call just before the delay starts.
            on_done (Optional[Callable[[], Any]]): Function to call after the function execution completes.
            *args: Positional arguments passed to the function.
            **kwargs: Keyword arguments passed to the function.
        """
        self.counter += 1
        # abort any running task
        if self.task and not self.task.done():
            self.task.cancel()

        async def task_func():
            if on_start:
                on_start()
            # debounce by waiting
            if self.counter > 1:
                await asyncio.sleep(self.delay)
            try:
                if asyncio.iscoroutinefunction(func):
                    self.log("coroutine", func, *args, **kwargs)
                    await func(*args, **kwargs)
                elif self.debounce_cpu_bound:
                    self.log("cpu_bound", func, *args, **kwargs)
                    await run.cpu_bound(func, *args, **kwargs)
                else:
                    self.log("io_bound", func, *args, **kwargs)
                    await run.io_bound(func, *args, **kwargs)
            except Exception as e:
                print(f"Error during task execution: {e}")
            finally:
                if on_done:
                    on_done()

        self.task = background_tasks.create(task_func(), name=self.debounce_task_name)

__init__(delay=0.33, debounce_cpu_bound=False, debounce_task_name='Debounce Task', debug=False)

Initialize the Debouncer with a specific delay.

Parameters:

Name Type Description Default
delay float

The debouncing delay in seconds. Default is 0.330 seconds.

0.33
debounce_cpu_bound bool

If True, use CPU-bound execution; otherwise use I/O-bound execution.

False
debounce_task_name str

The name to use for the task.

'Debounce Task'
debug bool

if True show debug info

False
Source code in ngwidgets/debouncer.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(
    self,
    delay: float = 0.330,
    debounce_cpu_bound: bool = False,
    debounce_task_name: str = "Debounce Task",
    debug: bool = False,
):
    """
    Initialize the Debouncer with a specific delay.

    Args:
        delay (float): The debouncing delay in seconds. Default is 0.330 seconds.
        debounce_cpu_bound (bool): If True, use CPU-bound execution; otherwise use I/O-bound execution.
        debounce_task_name (str): The name to use for the task.
        debug(bool): if True show debug info
    """
    self.delay = delay
    self.counter = 0
    self.debounce_cpu_bound = debounce_cpu_bound
    self.debounce_task_name = debounce_task_name
    self.debug = debug
    self.task: Optional[asyncio.Task] = None

debounce(func, *args, on_start=None, on_done=None, **kwargs) async

Debounce the given function call, using either CPU-bound or I/O-bound execution based on the flag. Optional callbacks can be specified for execution at the start and end of the function.

Parameters:

Name Type Description Default
func Callable

The function to be debounced.

required
on_start Optional[Callable[[], Any]]

Function to call just before the delay starts.

None
on_done Optional[Callable[[], Any]]

Function to call after the function execution completes.

None
*args

Positional arguments passed to the function.

()
**kwargs

Keyword arguments passed to the function.

{}
Source code in ngwidgets/debouncer.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
async def debounce(
    self,
    func: Callable,
    *args,
    on_start: Optional[Callable[[], Any]] = None,
    on_done: Optional[Callable[[], Any]] = None,
    **kwargs,
):
    """
    Debounce the given function call, using either CPU-bound or I/O-bound execution based on the flag.
    Optional callbacks can be specified for execution at the start and end of the function.

    Args:
        func (Callable): The function to be debounced.
        on_start (Optional[Callable[[], Any]]): Function to call just before the delay starts.
        on_done (Optional[Callable[[], Any]]): Function to call after the function execution completes.
        *args: Positional arguments passed to the function.
        **kwargs: Keyword arguments passed to the function.
    """
    self.counter += 1
    # abort any running task
    if self.task and not self.task.done():
        self.task.cancel()

    async def task_func():
        if on_start:
            on_start()
        # debounce by waiting
        if self.counter > 1:
            await asyncio.sleep(self.delay)
        try:
            if asyncio.iscoroutinefunction(func):
                self.log("coroutine", func, *args, **kwargs)
                await func(*args, **kwargs)
            elif self.debounce_cpu_bound:
                self.log("cpu_bound", func, *args, **kwargs)
                await run.cpu_bound(func, *args, **kwargs)
            else:
                self.log("io_bound", func, *args, **kwargs)
                await run.io_bound(func, *args, **kwargs)
        except Exception as e:
            print(f"Error during task execution: {e}")
        finally:
            if on_done:
                on_done()

    self.task = background_tasks.create(task_func(), name=self.debounce_task_name)

log(call_type, func, *args, **kwargs)

log the call

Source code in ngwidgets/debouncer.py
43
44
45
46
47
48
49
50
def log(self, call_type: str, func, *args, **kwargs):
    """
    log the call
    """
    if self.debug:
        print(f"calling {call_type} #{self.counter}. time")
        print("function:", func.__name__)
        print("args:", args, kwargs)

DebouncerUI

A class to manage UI feedback for debouncing, using a specific UI container.

Source code in ngwidgets/debouncer.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
class DebouncerUI:
    """A class to manage UI feedback for debouncing, using a specific UI container."""

    def __init__(
        self,
        parent,
        delay: float = 0.330,
        debounce_cpu_bound: bool = False,
        debounce_task_name: str = "Debounce Task",
        debug: bool = False,
    ):
        """
        Initialize the Debouncer UI within a specified parent container.

        Args:
            parent: The container in which the UI feedback should be managed.
            delay (float): The debouncing delay in seconds.
            debounce_cpu_bound (bool): If True, use CPU-bound execution; otherwise use I/O-bound execution.
            debounce_task_name (str): The name to use for the task.
            debug(bool): if True show debug info
        """
        self.parent = parent
        self.debouncer = Debouncer(
            delay=delay,
            debounce_cpu_bound=debounce_cpu_bound,
            debounce_task_name=debounce_task_name,
            debug=debug,
        )
        self.spinner = None

    def debounce(
        self,
        func: Callable,
        *args,
        on_start: Optional[Callable[[], Any]] = None,
        on_done: Optional[Callable[[], Any]] = None,
        **kwargs,
    ):
        """
        Debounce the given function call, managing UI feedback appropriately.

        Args:
            func (Callable): The function to be debounced.
            *args: Positional arguments passed to the function.
            **kwargs: Keyword arguments passed to the function.
        """

        def ui_on_start():
            """
            Default on start behavior is to add a spinner.
            """
            with self.parent:
                if not self.spinner:
                    self.spinner = ui.spinner()
            if on_start:
                on_start()

        def ui_on_done():
            """
            Default behavior is to remove the spinner.
            """
            if on_done:
                on_done()
            if self.spinner:
                try:
                    self.spinner.delete()
                except Exception as _ex:
                    pass
                self.spinner = None

        debounce_result = self.debouncer.debounce(
            func, *args, on_start=ui_on_start, on_done=ui_on_done, **kwargs
        )
        return debounce_result

__init__(parent, delay=0.33, debounce_cpu_bound=False, debounce_task_name='Debounce Task', debug=False)

Initialize the Debouncer UI within a specified parent container.

Parameters:

Name Type Description Default
parent

The container in which the UI feedback should be managed.

required
delay float

The debouncing delay in seconds.

0.33
debounce_cpu_bound bool

If True, use CPU-bound execution; otherwise use I/O-bound execution.

False
debounce_task_name str

The name to use for the task.

'Debounce Task'
debug bool

if True show debug info

False
Source code in ngwidgets/debouncer.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def __init__(
    self,
    parent,
    delay: float = 0.330,
    debounce_cpu_bound: bool = False,
    debounce_task_name: str = "Debounce Task",
    debug: bool = False,
):
    """
    Initialize the Debouncer UI within a specified parent container.

    Args:
        parent: The container in which the UI feedback should be managed.
        delay (float): The debouncing delay in seconds.
        debounce_cpu_bound (bool): If True, use CPU-bound execution; otherwise use I/O-bound execution.
        debounce_task_name (str): The name to use for the task.
        debug(bool): if True show debug info
    """
    self.parent = parent
    self.debouncer = Debouncer(
        delay=delay,
        debounce_cpu_bound=debounce_cpu_bound,
        debounce_task_name=debounce_task_name,
        debug=debug,
    )
    self.spinner = None

debounce(func, *args, on_start=None, on_done=None, **kwargs)

Debounce the given function call, managing UI feedback appropriately.

Parameters:

Name Type Description Default
func Callable

The function to be debounced.

required
*args

Positional arguments passed to the function.

()
**kwargs

Keyword arguments passed to the function.

{}
Source code in ngwidgets/debouncer.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def debounce(
    self,
    func: Callable,
    *args,
    on_start: Optional[Callable[[], Any]] = None,
    on_done: Optional[Callable[[], Any]] = None,
    **kwargs,
):
    """
    Debounce the given function call, managing UI feedback appropriately.

    Args:
        func (Callable): The function to be debounced.
        *args: Positional arguments passed to the function.
        **kwargs: Keyword arguments passed to the function.
    """

    def ui_on_start():
        """
        Default on start behavior is to add a spinner.
        """
        with self.parent:
            if not self.spinner:
                self.spinner = ui.spinner()
        if on_start:
            on_start()

    def ui_on_done():
        """
        Default behavior is to remove the spinner.
        """
        if on_done:
            on_done()
        if self.spinner:
            try:
                self.spinner.delete()
            except Exception as _ex:
                pass
            self.spinner = None

    debounce_result = self.debouncer.debounce(
        func, *args, on_start=ui_on_start, on_done=ui_on_done, **kwargs
    )
    return debounce_result

dict_edit

Created on 2023-06-22

@author: wf

DictEdit

NiceGUI based user interface for dictionary or dataclass editing that can be customized for each field in a form.

Attributes:

Name Type Description
d Union[dict, dataclass]

The data to be edited, converted to a dict if a dataclass.

form_ui_def FormUiDef

The UI definition for the form (if any).

card card

The card element containing the form.

inputs Dict[str, Input]

A dictionary mapping field names to input elements.

Source code in ngwidgets/dict_edit.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
class DictEdit:
    """
    NiceGUI based user interface for dictionary or dataclass editing
    that can be customized for each field in a form.

    Attributes:
        d (Union[dict, dataclass]): The data to be edited, converted to a dict if a dataclass.
        form_ui_def (FormUiDef): The UI definition for the form (if any).
        card (ui.card): The card element containing the form.
        inputs (Dict[str, Input]): A dictionary mapping field names to input elements.
    """

    empty = "❓"

    def __init__(
        self,
        data_to_edit: Union[dict, dataclass],
        form_ui_def: Optional[FormUiDef] = None,
        customization: Optional[Dict[str, Dict[str, Any]]] = None,
    ):
        """
        Initialize the DictEdit instance with the given data and optional UI definition.

        Args:
            data_to_edit (Union[dict, dataclass]): The dictionary or dataclass to be edited.
            form_ui_def (Optional[FormUiDef]): The UI definition for the form. If not provided,
                                               it will be generated automatically.
            customization (Optional[Dict[str, Dict[str, Any]]]): Customizations for the form fields.
        """
        self.data_to_edit = data_to_edit
        self.d = asdict(data_to_edit) if is_dataclass(data_to_edit) else data_to_edit
        self.form_ui_def = form_ui_def or (
            FormUiDef.from_dataclass(data_to_edit)
            if is_dataclass(data_to_edit)
            else FormUiDef.from_dict(self.d)
        )
        if customization:
            self.customize(customization)
        self.setup()

    def customize(self, customization: Dict[str, Dict[str, Any]]):
        """
        Customizes the UI fields based on the given customization dictionary.

        Args:
            customization (Dict[str, Dict[str, Any]]): A dictionary where each key corresponds to
                                                       a field name, and the value is another dictionary
                                                       specifying 'label', 'size', and optionally 'validation'.

        Example:
            customization = {
                'given_name': {'label': 'Given Name', 'size': 50},
                'family_name': {'label': 'Family Name', 'size': 50}
            }
        """
        if "_form_" in customization:
            form_mod = customization["_form_"]
            self.form_ui_def.title = form_mod.get("title", self.form_ui_def.title)
            self.form_ui_def.icon = form_mod.get("icon", self.form_ui_def.icon)
        for field_name, mods in customization.items():
            field_def = self.form_ui_def.ui_fields.get(field_name, None)
            if field_def:
                field_def.label = mods.get("label", field_def.label)
                field_def.size = mods.get("size", field_def.size)
                if "validation" in mods:
                    field_def.validation = mods["validation"]

    def setup(self):
        """Sets up the UI by creating a card and expansion for the form based on form_ui_def."""
        with ui.card() as self.card:
            with ui.expansion(
                text=self.form_ui_def.title, icon=self.form_ui_def.icon
            ).classes("w-full") as self.expansion:
                self.inputs = self._create_inputs()
            if is_dataclass(self.data_to_edit) and hasattr(
                self.data_to_edit, "ui_label"
            ):
                bind_from(
                    self.expansion._props,
                    "label",
                    self.data_to_edit,
                    "ui_label",
                    backward=lambda x: f"{self.form_ui_def.title}: {x if x else DictEdit.empty}",
                )

    def open(self):
        """
        show the details of the dict edit
        """
        self.expansion.open()

    def close(self):
        """
        hide the details of the dict edit
        """
        self.expansion.close()

    def _create_inputs(self) -> Dict[str, Input]:
        """Creates input elements for the form based on the FormUiDef."""
        inputs = {}
        for field_def in self.form_ui_def.ui_fields.values():
            value = self.d.get(field_def.field_name)
            if field_def.field_type is str:
                input_field = ui.input(label=field_def.label, value=value).props(
                    f"size={field_def.size}"
                )  # Text input for strings
            elif field_def.field_type in [int, float]:
                input_field = ui.number(
                    label=field_def.label, value=value
                )  # Number input for ints and floats
            elif field_def.field_type is bool:
                input_field = ui.checkbox(
                    field_def.label, value=value
                )  # Checkbox for booleans
            elif field_def.field_type in [datetime, date]:
                with ui.input("Date") as input_field:
                    with input_field.add_slot("append"):
                        ui.icon("edit_calendar").on(
                            "click", lambda: menu.open()
                        ).classes("cursor-pointer")
                    with ui.menu() as menu:
                        ui.date().bind_value(input_field)
            else:
                input_field = ui.input(
                    label=field_def.label, value=value
                )  # Default to text input

            input_field.bind_value(self.d, field_def.field_name)
            if field_def.validation:
                input_field.validation = {"Invalid input": field_def.validation}
            inputs[field_def.field_name] = input_field
        return inputs

    def add_on_change_handler(self, key: str, handler: Callable):
        """Adds an on_change event handler to the input corresponding to the given key."""
        if key in self.inputs:
            self.inputs[key].on_change(handler)

__init__(data_to_edit, form_ui_def=None, customization=None)

Initialize the DictEdit instance with the given data and optional UI definition.

Parameters:

Name Type Description Default
data_to_edit Union[dict, dataclass]

The dictionary or dataclass to be edited.

required
form_ui_def Optional[FormUiDef]

The UI definition for the form. If not provided, it will be generated automatically.

None
customization Optional[Dict[str, Dict[str, Any]]]

Customizations for the form fields.

None
Source code in ngwidgets/dict_edit.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def __init__(
    self,
    data_to_edit: Union[dict, dataclass],
    form_ui_def: Optional[FormUiDef] = None,
    customization: Optional[Dict[str, Dict[str, Any]]] = None,
):
    """
    Initialize the DictEdit instance with the given data and optional UI definition.

    Args:
        data_to_edit (Union[dict, dataclass]): The dictionary or dataclass to be edited.
        form_ui_def (Optional[FormUiDef]): The UI definition for the form. If not provided,
                                           it will be generated automatically.
        customization (Optional[Dict[str, Dict[str, Any]]]): Customizations for the form fields.
    """
    self.data_to_edit = data_to_edit
    self.d = asdict(data_to_edit) if is_dataclass(data_to_edit) else data_to_edit
    self.form_ui_def = form_ui_def or (
        FormUiDef.from_dataclass(data_to_edit)
        if is_dataclass(data_to_edit)
        else FormUiDef.from_dict(self.d)
    )
    if customization:
        self.customize(customization)
    self.setup()

add_on_change_handler(key, handler)

Adds an on_change event handler to the input corresponding to the given key.

Source code in ngwidgets/dict_edit.py
204
205
206
207
def add_on_change_handler(self, key: str, handler: Callable):
    """Adds an on_change event handler to the input corresponding to the given key."""
    if key in self.inputs:
        self.inputs[key].on_change(handler)

close()

hide the details of the dict edit

Source code in ngwidgets/dict_edit.py
162
163
164
165
166
def close(self):
    """
    hide the details of the dict edit
    """
    self.expansion.close()

customize(customization)

Customizes the UI fields based on the given customization dictionary.

Parameters:

Name Type Description Default
customization Dict[str, Dict[str, Any]]

A dictionary where each key corresponds to a field name, and the value is another dictionary specifying 'label', 'size', and optionally 'validation'.

required
Example

customization = { 'given_name': {'label': 'Given Name', 'size': 50}, 'family_name': {'label': 'Family Name', 'size': 50} }

Source code in ngwidgets/dict_edit.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def customize(self, customization: Dict[str, Dict[str, Any]]):
    """
    Customizes the UI fields based on the given customization dictionary.

    Args:
        customization (Dict[str, Dict[str, Any]]): A dictionary where each key corresponds to
                                                   a field name, and the value is another dictionary
                                                   specifying 'label', 'size', and optionally 'validation'.

    Example:
        customization = {
            'given_name': {'label': 'Given Name', 'size': 50},
            'family_name': {'label': 'Family Name', 'size': 50}
        }
    """
    if "_form_" in customization:
        form_mod = customization["_form_"]
        self.form_ui_def.title = form_mod.get("title", self.form_ui_def.title)
        self.form_ui_def.icon = form_mod.get("icon", self.form_ui_def.icon)
    for field_name, mods in customization.items():
        field_def = self.form_ui_def.ui_fields.get(field_name, None)
        if field_def:
            field_def.label = mods.get("label", field_def.label)
            field_def.size = mods.get("size", field_def.size)
            if "validation" in mods:
                field_def.validation = mods["validation"]

open()

show the details of the dict edit

Source code in ngwidgets/dict_edit.py
156
157
158
159
160
def open(self):
    """
    show the details of the dict edit
    """
    self.expansion.open()

setup()

Sets up the UI by creating a card and expansion for the form based on form_ui_def.

Source code in ngwidgets/dict_edit.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def setup(self):
    """Sets up the UI by creating a card and expansion for the form based on form_ui_def."""
    with ui.card() as self.card:
        with ui.expansion(
            text=self.form_ui_def.title, icon=self.form_ui_def.icon
        ).classes("w-full") as self.expansion:
            self.inputs = self._create_inputs()
        if is_dataclass(self.data_to_edit) and hasattr(
            self.data_to_edit, "ui_label"
        ):
            bind_from(
                self.expansion._props,
                "label",
                self.data_to_edit,
                "ui_label",
                backward=lambda x: f"{self.form_ui_def.title}: {x if x else DictEdit.empty}",
            )

FieldUiDef dataclass

A generic user interface definition for a field.

Source code in ngwidgets/dict_edit.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@dataclass
class FieldUiDef:
    """
    A generic user interface definition for a field.
    """

    field_name: str
    label: str
    size: int = 80
    field_type: Optional[type] = None
    validation: Optional[Callable[[Any], bool]] = None

    @staticmethod
    def from_field(field) -> "FieldUiDef":
        """Automatically creates a FieldUiDef from a dataclass field"""
        return FieldUiDef(
            field_name=field.name, label=field.name, field_type=field.type
        )

    @staticmethod
    def from_key_value(key: str, value) -> "FieldUiDef":
        """Automatically create a FieldUiDef from a key,value pair"""
        # Choose a default type for None values, e.g., str
        field_type = type(value) if value is not None else str
        return FieldUiDef(field_name=key, label=key, field_type=field_type)

from_field(field) staticmethod

Automatically creates a FieldUiDef from a dataclass field

Source code in ngwidgets/dict_edit.py
28
29
30
31
32
33
@staticmethod
def from_field(field) -> "FieldUiDef":
    """Automatically creates a FieldUiDef from a dataclass field"""
    return FieldUiDef(
        field_name=field.name, label=field.name, field_type=field.type
    )

from_key_value(key, value) staticmethod

Automatically create a FieldUiDef from a key,value pair

Source code in ngwidgets/dict_edit.py
35
36
37
38
39
40
@staticmethod
def from_key_value(key: str, value) -> "FieldUiDef":
    """Automatically create a FieldUiDef from a key,value pair"""
    # Choose a default type for None values, e.g., str
    field_type = type(value) if value is not None else str
    return FieldUiDef(field_name=key, label=key, field_type=field_type)

FormUiDef dataclass

A definition for the entire form's UI.

Source code in ngwidgets/dict_edit.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@dataclass
class FormUiDef:
    """
    A definition for the entire form's UI.
    """

    title: str
    icon: Optional[str] = "house"
    ui_fields: Dict[str, FieldUiDef] = field(default_factory=dict)

    @staticmethod
    def from_dataclass(data: dataclass) -> "FormUiDef":
        """Automatically creates a FormUiDef from a dataclass."""
        ui_fields = {}
        for field in fields(data):
            ui_fields[field.name] = FieldUiDef.from_field(field)
        return FormUiDef(title=data.__class__.__name__, ui_fields=ui_fields)

    @staticmethod
    def from_dict(dictionary: dict) -> "FormUiDef":
        """Automatically creates a FormUiDef from a dictionary."""
        ui_fields = {
            key: FieldUiDef.from_key_value(key, value)
            for key, value in dictionary.items()
        }
        return FormUiDef(title="Dictionary Form", ui_fields=ui_fields)

from_dataclass(data) staticmethod

Automatically creates a FormUiDef from a dataclass.

Source code in ngwidgets/dict_edit.py
53
54
55
56
57
58
59
@staticmethod
def from_dataclass(data: dataclass) -> "FormUiDef":
    """Automatically creates a FormUiDef from a dataclass."""
    ui_fields = {}
    for field in fields(data):
        ui_fields[field.name] = FieldUiDef.from_field(field)
    return FormUiDef(title=data.__class__.__name__, ui_fields=ui_fields)

from_dict(dictionary) staticmethod

Automatically creates a FormUiDef from a dictionary.

Source code in ngwidgets/dict_edit.py
61
62
63
64
65
66
67
68
@staticmethod
def from_dict(dictionary: dict) -> "FormUiDef":
    """Automatically creates a FormUiDef from a dictionary."""
    ui_fields = {
        key: FieldUiDef.from_key_value(key, value)
        for key, value in dictionary.items()
    }
    return FormUiDef(title="Dictionary Form", ui_fields=ui_fields)

editor

Created on 2022-11-27

@author: wf

Editor

helper class to open the system defined editor

see https://stackoverflow.com/questions/1442841/lauch-default-editor-like-webbrowser-module

Source code in ngwidgets/editor.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class Editor:
    """
    helper class to open the system defined editor

    see https://stackoverflow.com/questions/1442841/lauch-default-editor-like-webbrowser-module
    """

    @classmethod
    def open_filepath(cls, filepath: str):
        if platform.system() == "Darwin":  # macOS
            subprocess.call(("open", filepath))
        elif platform.system() == "Windows":  # Windows
            os.startfile(filepath, "open")
        else:  # linux variants
            subprocess.call(("xdg-open", filepath))

    @classmethod
    def extract_text(cls, html_text: str) -> str:
        """
        extract the text from the given html_text

        Args:
            html_text(str): the input for the html text

        Returns:
            str: the plain text
        """
        soup = BeautifulSoup(html_text, features="html.parser")

        # kill all script and style elements
        for script in soup(["script", "style"]):
            script.extract()  # rip it out

        # get text
        text = soup.get_text()

        # break into lines and remove leading and trailing space on each
        lines = (line.strip() for line in text.splitlines())
        # break multi-headlines into a line each
        chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
        # drop blank lines
        text = "\n".join(chunk for chunk in chunks if chunk)
        return text

    @classmethod
    def open(
        cls,
        file_source: str,
        extract_text: bool = True,
        default_editor_cmd: str = "/usr/local/bin/atom",
    ) -> str:
        """
        open an editor for the given file_source

        Args:
            file_source(str): the path to the file
            extract_text(bool): if True extract the text from html sources

        Returns:
            str: the path to the file e.g. a temporary file if the file_source points to an url
        """
        # handle urls
        # https://stackoverflow.com/a/45886824/1497139
        if file_source.startswith("http"):
            url_source = urlopen(file_source)
            # https://stackoverflow.com/a/19156107/1497139
            charset = url_source.headers.get_content_charset()
            # if charset fails here you might want to set it to utf-8 as a default!
            text = url_source.read().decode(charset)
            if extract_text:
                # https://stackoverflow.com/a/24618186/1497139
                text = cls.extract_text(text)

            return cls.open_tmp_text(text)

        editor_cmd = None
        editor_env = os.getenv("EDITOR")
        if editor_env:
            editor_cmd = editor_env
        if platform.system() == "Darwin":
            if not editor_env:
                # https://stackoverflow.com/questions/22390709/how-can-i-open-the-atom-editor-from-the-command-line-in-os-x
                editor_cmd = default_editor_cmd
        if editor_cmd:
            os_cmd = f"{editor_cmd} {file_source}"
            os.system(os_cmd)
        return file_source

    @classmethod
    def open_tmp_text(cls, text: str, file_name: str = None) -> str:
        """
        open an editor for the given text in a newly created temporary file

        Args:
            text(str): the text to write to a temporary file and then open
            file_name(str): the name to use for the file

        Returns:
            str: the path to the temp file
        """
        # see https://stackoverflow.com/a/8577226/1497139
        # https://stackoverflow.com/a/3924253/1497139
        with tempfile.NamedTemporaryFile(delete=False) as tmp:
            with open(tmp.name, "w") as tmp_file:
                tmp_file.write(text)
                tmp_file.close()
            if file_name is None:
                file_path = tmp.name
            else:
                # https://stackoverflow.com/questions/3167154/how-to-split-a-dos-path-into-its-components-in-python
                path = Path(tmp.name)
                # https://stackoverflow.com/a/49798311/1497139
                file_path = path.parent / file_name
                os.rename(tmp.name, file_path)

            return cls.open(str(file_path))

extract_text(html_text) classmethod

extract the text from the given html_text

Parameters:

Name Type Description Default
html_text str

the input for the html text

required

Returns:

Name Type Description
str str

the plain text

Source code in ngwidgets/editor.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@classmethod
def extract_text(cls, html_text: str) -> str:
    """
    extract the text from the given html_text

    Args:
        html_text(str): the input for the html text

    Returns:
        str: the plain text
    """
    soup = BeautifulSoup(html_text, features="html.parser")

    # kill all script and style elements
    for script in soup(["script", "style"]):
        script.extract()  # rip it out

    # get text
    text = soup.get_text()

    # break into lines and remove leading and trailing space on each
    lines = (line.strip() for line in text.splitlines())
    # break multi-headlines into a line each
    chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
    # drop blank lines
    text = "\n".join(chunk for chunk in chunks if chunk)
    return text

open(file_source, extract_text=True, default_editor_cmd='/usr/local/bin/atom') classmethod

open an editor for the given file_source

Parameters:

Name Type Description Default
file_source str

the path to the file

required
extract_text bool

if True extract the text from html sources

True

Returns:

Name Type Description
str str

the path to the file e.g. a temporary file if the file_source points to an url

Source code in ngwidgets/editor.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
@classmethod
def open(
    cls,
    file_source: str,
    extract_text: bool = True,
    default_editor_cmd: str = "/usr/local/bin/atom",
) -> str:
    """
    open an editor for the given file_source

    Args:
        file_source(str): the path to the file
        extract_text(bool): if True extract the text from html sources

    Returns:
        str: the path to the file e.g. a temporary file if the file_source points to an url
    """
    # handle urls
    # https://stackoverflow.com/a/45886824/1497139
    if file_source.startswith("http"):
        url_source = urlopen(file_source)
        # https://stackoverflow.com/a/19156107/1497139
        charset = url_source.headers.get_content_charset()
        # if charset fails here you might want to set it to utf-8 as a default!
        text = url_source.read().decode(charset)
        if extract_text:
            # https://stackoverflow.com/a/24618186/1497139
            text = cls.extract_text(text)

        return cls.open_tmp_text(text)

    editor_cmd = None
    editor_env = os.getenv("EDITOR")
    if editor_env:
        editor_cmd = editor_env
    if platform.system() == "Darwin":
        if not editor_env:
            # https://stackoverflow.com/questions/22390709/how-can-i-open-the-atom-editor-from-the-command-line-in-os-x
            editor_cmd = default_editor_cmd
    if editor_cmd:
        os_cmd = f"{editor_cmd} {file_source}"
        os.system(os_cmd)
    return file_source

open_tmp_text(text, file_name=None) classmethod

open an editor for the given text in a newly created temporary file

Parameters:

Name Type Description Default
text str

the text to write to a temporary file and then open

required
file_name str

the name to use for the file

None

Returns:

Name Type Description
str str

the path to the temp file

Source code in ngwidgets/editor.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@classmethod
def open_tmp_text(cls, text: str, file_name: str = None) -> str:
    """
    open an editor for the given text in a newly created temporary file

    Args:
        text(str): the text to write to a temporary file and then open
        file_name(str): the name to use for the file

    Returns:
        str: the path to the temp file
    """
    # see https://stackoverflow.com/a/8577226/1497139
    # https://stackoverflow.com/a/3924253/1497139
    with tempfile.NamedTemporaryFile(delete=False) as tmp:
        with open(tmp.name, "w") as tmp_file:
            tmp_file.write(text)
            tmp_file.close()
        if file_name is None:
            file_path = tmp.name
        else:
            # https://stackoverflow.com/questions/3167154/how-to-split-a-dos-path-into-its-components-in-python
            path = Path(tmp.name)
            # https://stackoverflow.com/a/49798311/1497139
            file_path = path.parent / file_name
            os.rename(tmp.name, file_path)

        return cls.open(str(file_path))

file_selector

Created on 2023-07-24

@author: wf

FileSelector

nicegui FileSelector

Source code in ngwidgets/file_selector.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
class FileSelector:
    """
    nicegui FileSelector
    """

    def __init__(
        self,
        path: str,
        extensions: dict = None,
        handler: Callable = None,
        filter_func: Callable[[str], bool] = None,
        create_ui: bool = True,
    ):
        """
        constructor

        Args:
            path (str): The path to the directory to start building the tree from.
            extensions(dict): the extensions to filter for as a dictionary with name as key and extension as value
            handler(Callable): handler function to call on selection
            filter_func(Callable): optional filter function
            create_ui(bool): if True create the self.tree ui.tree nicegui component - allows for testing the structure without ui by setting to False
        """
        self.path = path
        if extensions is None:
            extensions = {"srt": ".SRT", "gpx": ".gpx"}
        self.extensions = extensions
        self.handler = handler
        self.filter_func = filter_func
        # Initialize the file_count property
        self.file_count = 0
        # generate the tree structure
        self.tree_structure = self.get_dir_tree(self.path, self.extensions)

        if create_ui:
            # create the ui.tree object
            self.tree = ui.tree(
                [self.tree_structure], label_key="label", on_select=self.select_file
            )

    def get_path_items(self, path: str) -> List[str]:
        """
        Get sorted list of items in a specified directory path, filtering out non-relevant files like `._` files.

        Args:
            path (str): The directory path to list items from.

        Returns:
            List[str]: A sorted list of relevant items from the directory.
                       Returns an empty list if an error occurs.

        """
        items = []

        try:
            all_items = os.listdir(path)

            for item in all_items:
                if not item.startswith("._"):
                    if not self.filter_func or self.filter_func(item):
                        items.append(item)

            items.sort()
        except BaseException:
            pass

        return items

    def get_dir_tree(
        self, path: str, extensions: dict, id_path: List[int] = [1]
    ) -> Optional[Dict[str, Union[str, List[Dict]]]]:
        """
        Recursive function to construct a directory tree.

        Args:
            path (str): The path to the directory to start building the tree from.
            extensions(dict): the extensions to filter for as a dictionary with name as key and extension as value
            id_path (List[int]): List of integers representing the current path in the tree.

        Returns:
            dict: A dictionary representing the directory tree. For each directory or .scad file found,
            it will add a dictionary to the 'children' list of its parent directory's dictionary.
        """
        path = os.path.abspath(path)
        id_string = ".".join(map(str, id_path))
        items = self.get_path_items(path)
        children = []
        item_counter = 1  # counter for generating child id

        # Iterating over directories first
        for name in items:
            item_path = os.path.join(path, name)
            child_id_path = id_path + [item_counter]

            if os.path.isdir(item_path):
                dir_tree = self.get_dir_tree(item_path, extensions, child_id_path)
                if dir_tree:
                    children.append(dir_tree)
                    item_counter += 1

        # Then iterating over files
        # Check if an empty string is an allowed extension
        allow_no_extension = "" in extensions.values()
        for name in items:
            item_path = os.path.join(path, name)
            child_id_path = id_path + [item_counter]

            # make sure the item is a file
            if not os.path.isdir(item_path):

                # Check if the item has an extension that matches the allowed extensions
                has_allowed_extension = any(
                    name.endswith(ext) for ext in extensions.values() if ext
                )

                # Check if the item has no extension and no extension is allowed
                is_no_extension_allowed = allow_no_extension and "." not in name

                # Proceed if the item either has an allowed extension or no extension is allowed
                if has_allowed_extension or is_no_extension_allowed:
                    children.append(
                        {
                            "id": ".".join(map(str, child_id_path)),
                            "label": name,
                            "value": item_path,
                        }
                    )
                    self.file_count += 1  # Increment file_count for each file added

                    item_counter += 1

        if children:
            return {
                "id": id_string,
                "label": os.path.basename(path),
                "value": path,
                "children": children,
            }

    def find_node_by_id(
        self, tree: Dict[str, Union[str, List[Dict]]], id_to_find: str
    ) -> Optional[Dict[str, Union[str, List[Dict]]]]:
        """
        Recursive function to find a node (file or directory) by its ID in a directory tree.

        Args:
            tree (dict): A dictionary representing the directory tree. The tree is constructed with each node
                containing 'id' (str) as a unique identifier, 'value' (str) as the path to the file or directory,
                and 'children' (list of dict) as a list of child nodes.
            id_to_find (str): The ID of the node to find in the directory tree.

        Returns:
            dict: The node associated with the found ID. Returns None if the ID is not found.
        """
        if tree["id"] == id_to_find:
            return tree

        for child in tree.get("children", []):
            found = self.find_node_by_id(child, id_to_find)
            if found:
                return found

        return None

    def expand(self, node_ids: List[str]) -> None:
        """
        expand the nodes with the given ids

        node_ids(list): the list of node ids to be expanded
        """
        self.tree._props["expanded"] = node_ids
        self.tree.update()

    async def select_file(self, vcea: ValueChangeEventArguments):
        """
        select the given file and call my handler on the file path of it

        Args:
            vcea(ValueChangeEventArguments): the tree selection event
        """
        id_to_find = vcea.value  # Assuming vcea.value contains the id
        if id_to_find is None:
            return

        selected_node = self.find_node_by_id(self.tree_structure, id_to_find)
        if selected_node is None:
            raise ValueError(
                f"No item with id {id_to_find} found in the tree structure."
            )

        file_path = selected_node["value"]

        # Only expand the node if it is a directory
        if os.path.isdir(file_path):
            # Access the children of the selected node and get their ids
            if "children" in selected_node:
                child_ids = [child["id"] for child in selected_node["children"]]
                self.expand(child_ids)
        else:
            if self.handler:
                if inspect.iscoroutinefunction(self.handler):
                    await self.handler(file_path)
                else:
                    self.handler(file_path)
                # Close all nodes if it is a file
            self.expand([])

__init__(path, extensions=None, handler=None, filter_func=None, create_ui=True)

constructor

Parameters:

Name Type Description Default
path str

The path to the directory to start building the tree from.

required
extensions dict

the extensions to filter for as a dictionary with name as key and extension as value

None
handler Callable

handler function to call on selection

None
filter_func Callable

optional filter function

None
create_ui bool

if True create the self.tree ui.tree nicegui component - allows for testing the structure without ui by setting to False

True
Source code in ngwidgets/file_selector.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def __init__(
    self,
    path: str,
    extensions: dict = None,
    handler: Callable = None,
    filter_func: Callable[[str], bool] = None,
    create_ui: bool = True,
):
    """
    constructor

    Args:
        path (str): The path to the directory to start building the tree from.
        extensions(dict): the extensions to filter for as a dictionary with name as key and extension as value
        handler(Callable): handler function to call on selection
        filter_func(Callable): optional filter function
        create_ui(bool): if True create the self.tree ui.tree nicegui component - allows for testing the structure without ui by setting to False
    """
    self.path = path
    if extensions is None:
        extensions = {"srt": ".SRT", "gpx": ".gpx"}
    self.extensions = extensions
    self.handler = handler
    self.filter_func = filter_func
    # Initialize the file_count property
    self.file_count = 0
    # generate the tree structure
    self.tree_structure = self.get_dir_tree(self.path, self.extensions)

    if create_ui:
        # create the ui.tree object
        self.tree = ui.tree(
            [self.tree_structure], label_key="label", on_select=self.select_file
        )

expand(node_ids)

expand the nodes with the given ids

node_ids(list): the list of node ids to be expanded

Source code in ngwidgets/file_selector.py
179
180
181
182
183
184
185
186
def expand(self, node_ids: List[str]) -> None:
    """
    expand the nodes with the given ids

    node_ids(list): the list of node ids to be expanded
    """
    self.tree._props["expanded"] = node_ids
    self.tree.update()

find_node_by_id(tree, id_to_find)

Recursive function to find a node (file or directory) by its ID in a directory tree.

Parameters:

Name Type Description Default
tree dict

A dictionary representing the directory tree. The tree is constructed with each node containing 'id' (str) as a unique identifier, 'value' (str) as the path to the file or directory, and 'children' (list of dict) as a list of child nodes.

required
id_to_find str

The ID of the node to find in the directory tree.

required

Returns:

Name Type Description
dict Optional[Dict[str, Union[str, List[Dict]]]]

The node associated with the found ID. Returns None if the ID is not found.

Source code in ngwidgets/file_selector.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def find_node_by_id(
    self, tree: Dict[str, Union[str, List[Dict]]], id_to_find: str
) -> Optional[Dict[str, Union[str, List[Dict]]]]:
    """
    Recursive function to find a node (file or directory) by its ID in a directory tree.

    Args:
        tree (dict): A dictionary representing the directory tree. The tree is constructed with each node
            containing 'id' (str) as a unique identifier, 'value' (str) as the path to the file or directory,
            and 'children' (list of dict) as a list of child nodes.
        id_to_find (str): The ID of the node to find in the directory tree.

    Returns:
        dict: The node associated with the found ID. Returns None if the ID is not found.
    """
    if tree["id"] == id_to_find:
        return tree

    for child in tree.get("children", []):
        found = self.find_node_by_id(child, id_to_find)
        if found:
            return found

    return None

get_dir_tree(path, extensions, id_path=[1])

Recursive function to construct a directory tree.

Parameters:

Name Type Description Default
path str

The path to the directory to start building the tree from.

required
extensions dict

the extensions to filter for as a dictionary with name as key and extension as value

required
id_path List[int]

List of integers representing the current path in the tree.

[1]

Returns:

Name Type Description
dict Optional[Dict[str, Union[str, List[Dict]]]]

A dictionary representing the directory tree. For each directory or .scad file found,

Optional[Dict[str, Union[str, List[Dict]]]]

it will add a dictionary to the 'children' list of its parent directory's dictionary.

Source code in ngwidgets/file_selector.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def get_dir_tree(
    self, path: str, extensions: dict, id_path: List[int] = [1]
) -> Optional[Dict[str, Union[str, List[Dict]]]]:
    """
    Recursive function to construct a directory tree.

    Args:
        path (str): The path to the directory to start building the tree from.
        extensions(dict): the extensions to filter for as a dictionary with name as key and extension as value
        id_path (List[int]): List of integers representing the current path in the tree.

    Returns:
        dict: A dictionary representing the directory tree. For each directory or .scad file found,
        it will add a dictionary to the 'children' list of its parent directory's dictionary.
    """
    path = os.path.abspath(path)
    id_string = ".".join(map(str, id_path))
    items = self.get_path_items(path)
    children = []
    item_counter = 1  # counter for generating child id

    # Iterating over directories first
    for name in items:
        item_path = os.path.join(path, name)
        child_id_path = id_path + [item_counter]

        if os.path.isdir(item_path):
            dir_tree = self.get_dir_tree(item_path, extensions, child_id_path)
            if dir_tree:
                children.append(dir_tree)
                item_counter += 1

    # Then iterating over files
    # Check if an empty string is an allowed extension
    allow_no_extension = "" in extensions.values()
    for name in items:
        item_path = os.path.join(path, name)
        child_id_path = id_path + [item_counter]

        # make sure the item is a file
        if not os.path.isdir(item_path):

            # Check if the item has an extension that matches the allowed extensions
            has_allowed_extension = any(
                name.endswith(ext) for ext in extensions.values() if ext
            )

            # Check if the item has no extension and no extension is allowed
            is_no_extension_allowed = allow_no_extension and "." not in name

            # Proceed if the item either has an allowed extension or no extension is allowed
            if has_allowed_extension or is_no_extension_allowed:
                children.append(
                    {
                        "id": ".".join(map(str, child_id_path)),
                        "label": name,
                        "value": item_path,
                    }
                )
                self.file_count += 1  # Increment file_count for each file added

                item_counter += 1

    if children:
        return {
            "id": id_string,
            "label": os.path.basename(path),
            "value": path,
            "children": children,
        }

get_path_items(path)

Get sorted list of items in a specified directory path, filtering out non-relevant files like ._ files.

Parameters:

Name Type Description Default
path str

The directory path to list items from.

required

Returns:

Type Description
List[str]

List[str]: A sorted list of relevant items from the directory. Returns an empty list if an error occurs.

Source code in ngwidgets/file_selector.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def get_path_items(self, path: str) -> List[str]:
    """
    Get sorted list of items in a specified directory path, filtering out non-relevant files like `._` files.

    Args:
        path (str): The directory path to list items from.

    Returns:
        List[str]: A sorted list of relevant items from the directory.
                   Returns an empty list if an error occurs.

    """
    items = []

    try:
        all_items = os.listdir(path)

        for item in all_items:
            if not item.startswith("._"):
                if not self.filter_func or self.filter_func(item):
                    items.append(item)

        items.sort()
    except BaseException:
        pass

    return items

select_file(vcea) async

select the given file and call my handler on the file path of it

Parameters:

Name Type Description Default
vcea ValueChangeEventArguments

the tree selection event

required
Source code in ngwidgets/file_selector.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
async def select_file(self, vcea: ValueChangeEventArguments):
    """
    select the given file and call my handler on the file path of it

    Args:
        vcea(ValueChangeEventArguments): the tree selection event
    """
    id_to_find = vcea.value  # Assuming vcea.value contains the id
    if id_to_find is None:
        return

    selected_node = self.find_node_by_id(self.tree_structure, id_to_find)
    if selected_node is None:
        raise ValueError(
            f"No item with id {id_to_find} found in the tree structure."
        )

    file_path = selected_node["value"]

    # Only expand the node if it is a directory
    if os.path.isdir(file_path):
        # Access the children of the selected node and get their ids
        if "children" in selected_node:
            child_ids = [child["id"] for child in selected_node["children"]]
            self.expand(child_ids)
    else:
        if self.handler:
            if inspect.iscoroutinefunction(self.handler):
                await self.handler(file_path)
            else:
                self.handler(file_path)
            # Close all nodes if it is a file
        self.expand([])

gpxviewer

Created on 2025-01-17

@author: wf

GPXViewer

display a given gpx file

Source code in ngwidgets/gpxviewer.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
class GPXViewer:
    """
    display a given gpx file
    """

    samples = {
        "Mountain bike loop at Middlesex Fells reservation.": "https://www.topografix.com/fells_loop.gpx",
        "Via Verde de Arditurri": "https://www.bahntrassenradwege.de/images/Spanien/Baskenland/V-v-de-arditurri/Arditurri2.gpx",
        "Vía Verde del Fc Vasco Navarro": "https://ebike.bitplan.com/images/ebike/e/ec/VV-del-FC-Vasco-Navarro_fdmi1644.gpx",
    }
    default_center = [51.4934, 0.0098]  # greenwich
    default_zoom = 11

    @classmethod
    def get_parser(cls):
        """
        Configure and return argument parser
        """
        parser = argparse.ArgumentParser(description="GPX File Viewer")
        parser.add_argument("--gpx", required=False, help="URL or path to GPX file")
        parser.add_argument(
            "--token", required=False, help="Authentication token for GPX access"
        )
        parser.add_argument(
            "--port",
            type=int,
            default=8080,
            help="Port to run server on (default: 8080)",
        )
        parser.add_argument(
            "--zoom",
            type=int,
            default=GPXViewer.default_zoom,
            help="zoom level (default: 11)",
        )
        parser.add_argument(
            "--center",
            nargs=2,
            type=float,
            default=GPXViewer.default_center,
            help="center lat,lon - default: Greenwich",
        )
        parser.add_argument("--debug", action="store_true", help="Show debug output")
        return parser

    @classmethod
    def from_url(cls, gpx_url) -> "GPXViewer":
        viewer = cls()
        viewer.load_gpx(gpx_url)
        return viewer

    def __init__(self, args: argparse.Namespace = None):
        """
        constructor

        Args:
            args(argparse.Namespace): command line arguments
        """
        self.args = args
        self.tour = None
        self.leg_styles = LegStyles.default()
        self.set_center()
        if args:
            self.debug = args.debug
            self.token = args.token
            self.center = args.center
            self.zoom = args.zoom
            if self.args.gpx:
                self.load_gpx(self.args.gpx)
        else:
            self.zoom = GPXViewer.default_zoom
            self.center = GPXViewer.default_center

    def load_gpx(self, gpx_url: str):
        """
        load the given gpx file
        """
        response = requests.get(gpx_url)
        self.gpx = gpxpy.parse(response.text)
        self.get_points(self.gpx)
        return self.gpx

    def set_center(self):
        """
        Calculate and set the center and bounding box based on tour legs
        """

        if self.tour and self.tour.legs:
            points = []
            for leg in self.tour.legs:
                points.append(leg.start.coordinates)
                points.append(leg.end.coordinates)
            # Wrong order: lats, lons need to be extracted after zipping
            lats = [p[0] for p in points]
            lons = [p[1] for p in points]
            self.bounding_box = (min(lats), max(lats), min(lons), max(lons))
            self.center = ((min(lats) + max(lats)) / 2, (min(lons) + max(lons)) / 2)
        else:
            self.center = self.default_center
            self.bounding_box = None
        return self.center

    def add_leg(
        self,
        start_point,
        end_point,
        leg_type: str,
        add_end_point: bool = False,
        url: Optional[str] = None,
    ):
        """
        Add a leg to the tour

        Args:
            start_point: GPX point for start of leg
            end_point: GPX point for end of leg
            leg_type: Type of leg (e.g., "bike", "train", "car")
            add_end_point: Whether to add the end point (True for last leg)
            url: Optional URL associated with the leg
        """
        if self.tour is None:
            self.tour = Tour(name="GPX Tour")

        # Create locations
        start_loc = Loc(
            id=str(len(self.tour.legs)),
            name=start_point.name if hasattr(start_point, "name") else None,
            coordinates=(start_point.latitude, start_point.longitude),
        )
        end_loc = Loc(
            id=str(len(self.tour.legs) + 1),
            name=end_point.name if hasattr(end_point, "name") else None,
            coordinates=(end_point.latitude, end_point.longitude),
        )

        # Create and add leg
        leg = Leg(leg_type=leg_type, start=start_loc, end=end_loc, url=url)
        self.tour.legs.append(leg)

    def get_points(self, gpx, way_points_fallback: bool = False):
        """
        Extract waypoints and legs from the GPX object and create a tour
        """
        self.tour = Tour(name="GPX Tour")

        # Process routes
        for route in gpx.routes:
            for i in range(len(route.points) - 1):
                url = route.link.href if route.link else None
                is_last = i == len(route.points) - 2
                self.add_leg(
                    route.points[i],
                    route.points[i + 1],
                    "bike",  # Default to bike for routes
                    add_end_point=is_last,
                    url=url,
                )

        # Process tracks
        for track in gpx.tracks:
            for segment in track.segments:
                for i in range(len(segment.points) - 1):
                    is_last = i == len(segment.points) - 2
                    self.add_leg(
                        segment.points[i],
                        segment.points[i + 1],
                        "bike",  # Default to bike for tracks
                        add_end_point=is_last,
                    )

        prev_loc = None
        # Handle waypoints if no legs were created and fallback is active
        if not self.tour.legs and gpx.waypoints and way_points_fallback:
            for i, waypoint in enumerate(gpx.waypoints):
                loc = Loc(
                    id=str(i),
                    name=waypoint.name,
                    coordinates=(waypoint.latitude, waypoint.longitude),
                    notes=waypoint.description,
                )
                if i > 0:
                    leg = Leg(
                        leg_type="bike",
                        start=prev_loc,
                        end=loc,
                        url=waypoint.link.href if waypoint.link else None,
                    )
                    self.tour.legs.append(leg)
                prev_loc = loc

    def parse_lines(self, lines: str):
        """
        Parse the 'lines' parameter into route segments.

        Args:
            lines (str): The input string containing routes in the format:
                "By bike: 51.243931° N, 6.520022° E, 51.269222° N, 6.625467° E:"

        Returns:
            Tour: Created tour from the parsed coordinates
        """
        coordinate_pattern = r"(\d+\.\d+)° ([NS]), (\d+\.\d+)° ([EW])"
        route_segments = lines.split(":")
        self.tour = Tour(name="Parsed Tour")

        for segment in route_segments:
            segment = segment.strip()

            # Extract leg type (e.g., "bike", "train")
            leg_type_match = re.match(r"By (\w+)→", segment)
            if leg_type_match:
                leg_type = leg_type_match.group(1).lower()
                pass
            else:
                leg_type = "bike"

            points = re.findall(coordinate_pattern, segment)
            if points:
                prev_loc = None
                for i, (lat, ns, lon, ew) in enumerate(points):
                    lat_val = float(lat) * (-1 if ns == "S" else 1)
                    lon_val = float(lon) * (-1 if ew == "W" else 1)

                    loc = Loc(
                        id=str(len(self.tour.legs) + i),
                        name=None,
                        coordinates=(lat_val, lon_val),
                    )

                    if prev_loc:
                        leg = Leg(leg_type=leg_type, start=prev_loc, end=loc)
                        self.tour.legs.append(leg)
                    prev_loc = loc

        if not self.tour.legs:
            raise ValueError("No valid routes found in the input lines.")

        return self.tour

    def parse_lines_and_show(self, lines: str, zoom: int = None):
        """
        Parse lines and display them on the map
        """
        self.parse_lines(lines)
        self.show(zoom=zoom)

    def show(self, zoom: int = None, center=None):
        """
        Show tour with styled paths
        """
        if zoom is None:
            zoom = self.zoom
        if center is None:
            center = self.set_center()

        self.map = LeafletMap(center=center, zoom=zoom)
        if self.tour:
            self.map.draw_tour(self.tour, self.leg_styles)

__init__(args=None)

constructor

Parameters:

Name Type Description Default
args Namespace

command line arguments

None
Source code in ngwidgets/gpxviewer.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def __init__(self, args: argparse.Namespace = None):
    """
    constructor

    Args:
        args(argparse.Namespace): command line arguments
    """
    self.args = args
    self.tour = None
    self.leg_styles = LegStyles.default()
    self.set_center()
    if args:
        self.debug = args.debug
        self.token = args.token
        self.center = args.center
        self.zoom = args.zoom
        if self.args.gpx:
            self.load_gpx(self.args.gpx)
    else:
        self.zoom = GPXViewer.default_zoom
        self.center = GPXViewer.default_center

add_leg(start_point, end_point, leg_type, add_end_point=False, url=None)

Add a leg to the tour

Parameters:

Name Type Description Default
start_point

GPX point for start of leg

required
end_point

GPX point for end of leg

required
leg_type str

Type of leg (e.g., "bike", "train", "car")

required
add_end_point bool

Whether to add the end point (True for last leg)

False
url Optional[str]

Optional URL associated with the leg

None
Source code in ngwidgets/gpxviewer.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def add_leg(
    self,
    start_point,
    end_point,
    leg_type: str,
    add_end_point: bool = False,
    url: Optional[str] = None,
):
    """
    Add a leg to the tour

    Args:
        start_point: GPX point for start of leg
        end_point: GPX point for end of leg
        leg_type: Type of leg (e.g., "bike", "train", "car")
        add_end_point: Whether to add the end point (True for last leg)
        url: Optional URL associated with the leg
    """
    if self.tour is None:
        self.tour = Tour(name="GPX Tour")

    # Create locations
    start_loc = Loc(
        id=str(len(self.tour.legs)),
        name=start_point.name if hasattr(start_point, "name") else None,
        coordinates=(start_point.latitude, start_point.longitude),
    )
    end_loc = Loc(
        id=str(len(self.tour.legs) + 1),
        name=end_point.name if hasattr(end_point, "name") else None,
        coordinates=(end_point.latitude, end_point.longitude),
    )

    # Create and add leg
    leg = Leg(leg_type=leg_type, start=start_loc, end=end_loc, url=url)
    self.tour.legs.append(leg)

get_parser() classmethod

Configure and return argument parser

Source code in ngwidgets/gpxviewer.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@classmethod
def get_parser(cls):
    """
    Configure and return argument parser
    """
    parser = argparse.ArgumentParser(description="GPX File Viewer")
    parser.add_argument("--gpx", required=False, help="URL or path to GPX file")
    parser.add_argument(
        "--token", required=False, help="Authentication token for GPX access"
    )
    parser.add_argument(
        "--port",
        type=int,
        default=8080,
        help="Port to run server on (default: 8080)",
    )
    parser.add_argument(
        "--zoom",
        type=int,
        default=GPXViewer.default_zoom,
        help="zoom level (default: 11)",
    )
    parser.add_argument(
        "--center",
        nargs=2,
        type=float,
        default=GPXViewer.default_center,
        help="center lat,lon - default: Greenwich",
    )
    parser.add_argument("--debug", action="store_true", help="Show debug output")
    return parser

get_points(gpx, way_points_fallback=False)

Extract waypoints and legs from the GPX object and create a tour

Source code in ngwidgets/gpxviewer.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def get_points(self, gpx, way_points_fallback: bool = False):
    """
    Extract waypoints and legs from the GPX object and create a tour
    """
    self.tour = Tour(name="GPX Tour")

    # Process routes
    for route in gpx.routes:
        for i in range(len(route.points) - 1):
            url = route.link.href if route.link else None
            is_last = i == len(route.points) - 2
            self.add_leg(
                route.points[i],
                route.points[i + 1],
                "bike",  # Default to bike for routes
                add_end_point=is_last,
                url=url,
            )

    # Process tracks
    for track in gpx.tracks:
        for segment in track.segments:
            for i in range(len(segment.points) - 1):
                is_last = i == len(segment.points) - 2
                self.add_leg(
                    segment.points[i],
                    segment.points[i + 1],
                    "bike",  # Default to bike for tracks
                    add_end_point=is_last,
                )

    prev_loc = None
    # Handle waypoints if no legs were created and fallback is active
    if not self.tour.legs and gpx.waypoints and way_points_fallback:
        for i, waypoint in enumerate(gpx.waypoints):
            loc = Loc(
                id=str(i),
                name=waypoint.name,
                coordinates=(waypoint.latitude, waypoint.longitude),
                notes=waypoint.description,
            )
            if i > 0:
                leg = Leg(
                    leg_type="bike",
                    start=prev_loc,
                    end=loc,
                    url=waypoint.link.href if waypoint.link else None,
                )
                self.tour.legs.append(leg)
            prev_loc = loc

load_gpx(gpx_url)

load the given gpx file

Source code in ngwidgets/gpxviewer.py
91
92
93
94
95
96
97
98
def load_gpx(self, gpx_url: str):
    """
    load the given gpx file
    """
    response = requests.get(gpx_url)
    self.gpx = gpxpy.parse(response.text)
    self.get_points(self.gpx)
    return self.gpx

parse_lines(lines)

Parse the 'lines' parameter into route segments.

Parameters:

Name Type Description Default
lines str

The input string containing routes in the format: "By bike: 51.243931° N, 6.520022° E, 51.269222° N, 6.625467° E:"

required

Returns:

Name Type Description
Tour

Created tour from the parsed coordinates

Source code in ngwidgets/gpxviewer.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def parse_lines(self, lines: str):
    """
    Parse the 'lines' parameter into route segments.

    Args:
        lines (str): The input string containing routes in the format:
            "By bike: 51.243931° N, 6.520022° E, 51.269222° N, 6.625467° E:"

    Returns:
        Tour: Created tour from the parsed coordinates
    """
    coordinate_pattern = r"(\d+\.\d+)° ([NS]), (\d+\.\d+)° ([EW])"
    route_segments = lines.split(":")
    self.tour = Tour(name="Parsed Tour")

    for segment in route_segments:
        segment = segment.strip()

        # Extract leg type (e.g., "bike", "train")
        leg_type_match = re.match(r"By (\w+)→", segment)
        if leg_type_match:
            leg_type = leg_type_match.group(1).lower()
            pass
        else:
            leg_type = "bike"

        points = re.findall(coordinate_pattern, segment)
        if points:
            prev_loc = None
            for i, (lat, ns, lon, ew) in enumerate(points):
                lat_val = float(lat) * (-1 if ns == "S" else 1)
                lon_val = float(lon) * (-1 if ew == "W" else 1)

                loc = Loc(
                    id=str(len(self.tour.legs) + i),
                    name=None,
                    coordinates=(lat_val, lon_val),
                )

                if prev_loc:
                    leg = Leg(leg_type=leg_type, start=prev_loc, end=loc)
                    self.tour.legs.append(leg)
                prev_loc = loc

    if not self.tour.legs:
        raise ValueError("No valid routes found in the input lines.")

    return self.tour

parse_lines_and_show(lines, zoom=None)

Parse lines and display them on the map

Source code in ngwidgets/gpxviewer.py
257
258
259
260
261
262
def parse_lines_and_show(self, lines: str, zoom: int = None):
    """
    Parse lines and display them on the map
    """
    self.parse_lines(lines)
    self.show(zoom=zoom)

set_center()

Calculate and set the center and bounding box based on tour legs

Source code in ngwidgets/gpxviewer.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def set_center(self):
    """
    Calculate and set the center and bounding box based on tour legs
    """

    if self.tour and self.tour.legs:
        points = []
        for leg in self.tour.legs:
            points.append(leg.start.coordinates)
            points.append(leg.end.coordinates)
        # Wrong order: lats, lons need to be extracted after zipping
        lats = [p[0] for p in points]
        lons = [p[1] for p in points]
        self.bounding_box = (min(lats), max(lats), min(lons), max(lons))
        self.center = ((min(lats) + max(lats)) / 2, (min(lons) + max(lons)) / 2)
    else:
        self.center = self.default_center
        self.bounding_box = None
    return self.center

show(zoom=None, center=None)

Show tour with styled paths

Source code in ngwidgets/gpxviewer.py
264
265
266
267
268
269
270
271
272
273
274
275
def show(self, zoom: int = None, center=None):
    """
    Show tour with styled paths
    """
    if zoom is None:
        zoom = self.zoom
    if center is None:
        center = self.set_center()

    self.map = LeafletMap(center=center, zoom=zoom)
    if self.tour:
        self.map.draw_tour(self.tour, self.leg_styles)

image_cropper

Created on 2025-12-08

@author: wf

ImageCropper

A NiceGUI-based interactive image cropper. Allows users to draw a box on an image to define crop coordinates.

Source code in ngwidgets/image_cropper.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class ImageCropper:
    """
    A NiceGUI-based interactive image cropper.
    Allows users to draw a box on an image to define crop coordinates.
    """

    def __init__(self, solution):
        self.solution = solution
        # Crop State
        self.crop_x: int = 0
        self.crop_y: int = 0
        self.crop_width: int = 0
        self.crop_height: int = 0

        # Dragging State for UI
        self.dragging: bool = False
        self.start_x: float = 0
        self.start_y: float = 0

        # References
        self.interactive_view: ui.interactive_image = None
        self.container = None
        # Injections needed by caller
        self.file_path: str = None  # Store the physical path
        # Debugging checkpoint
        pass

    def setup_ui(self, container, image_url: str):
        """
        Sets up the UI elements.

        Args:
            container: The parent container
            image_url: The URL for the browser to display
        """
        self.container = container

        # Coordinate display
        with ui.row().classes("items-center"):
            ui.label("Crop Area:")
            with ui.row():
                ui.number(label="X", value=0).bind_value(self, "crop_x").props(
                    "size=5 readonly"
                )
                ui.number(label="Y", value=0).bind_value(self, "crop_y").props(
                    "size=5 readonly"
                )
                ui.number(label="W", value=0).bind_value(self, "crop_width").props(
                    "size=5 readonly"
                )
                ui.number(label="H", value=0).bind_value(self, "crop_height").props(
                    "size=5 readonly"
                )

            # Reset button convenient to have nearby
            ui.button(icon="crop_free", on_click=self.reset_crop).props(
                "flat round color=warning"
            ).tooltip("Reset Crop")
            # Crop button
            # Visibility is bound to crop_width. It appears only when width > 0.
            ui.button(icon="check", on_click=self.on_crop_click).props(
                "flat round color=positive"
            ).tooltip("Apply Crop").bind_visibility_from(
                self, "crop_width", backward=lambda w: w > 0
            )

            # Rotation buttons
            ui.button(
                icon="rotate_left", on_click=lambda: self.on_rotate_click(left=True)
            ).props("flat round color=primary").tooltip("Rotate Left 90°")
            ui.button(
                icon="rotate_right", on_click=lambda: self.on_rotate_click(left=False)
            ).props("flat round color=primary").tooltip("Rotate Right 90°")

        # Interactive Image implementation
        self.interactive_view = ui.interactive_image(
            image_url,
            on_mouse=self.handle_mouse,
            events=["mousedown", "mousemove", "mouseup"],
            cross=True,
        ).classes("w-full")

    def set_source(self, source: str):
        """Updates the image source URL."""
        if self.interactive_view:
            self.interactive_view.set_source(source)

    def handle_mouse(self, e: events.MouseEventArguments):
        """
        Handles mouse events to draw a crop rectangle on the interactive image.
        """
        if e.type == "mousedown":
            self.dragging = True
            self.start_x = e.image_x
            self.start_y = e.image_y

        elif e.type == "mousemove":
            if self.dragging:
                self.update_crop_selection(e.image_x, e.image_y)

        elif e.type == "mouseup":
            self.dragging = False
            self.update_crop_selection(e.image_x, e.image_y)

    def update_crop_selection(self, current_x: float, current_y: float):
        """
        Calculates the rectangle coordinates based on start and current pointers,
        updates state variables, and draws the SVG overlay.
        """
        # Calculate top-left corner and dimensions
        x = min(self.start_x, current_x)
        y = min(self.start_y, current_y)
        w = abs(self.start_x - current_x)
        h = abs(self.start_y - current_y)

        # Update persistent crop variables (convert to int for PIL)
        self.crop_x = int(x)
        self.crop_y = int(y)
        self.crop_width = int(w)
        self.crop_height = int(h)

        # Update the visual SVG overlay
        self.interactive_view.content = (
            f'<rect x="{x}" y="{y}" width="{w}" height="{h}" '
            f'fill="none" stroke="red" stroke-width="2" />'
        )

    def reset_crop(self):
        """Resets crop variables and clears SVG overlay."""
        self.crop_x = 0
        self.crop_y = 0
        self.crop_width = 0
        self.crop_height = 0
        if self.interactive_view:
            self.interactive_view.content = ""

    def notify_problem(self, msg: str):
        with self.container:
            ui.notify(msg, type="warning")

    async def on_crop_click(self):
        """
        handle the click of the crop button
        """
        if self.file_path and os.path.isfile(self.file_path):
            background_tasks.create(self.apply_crop())
        else:
            self.notify_problem("No file path configured for cropping.")

    async def on_rotate_click(self, left: bool = True):
        """
        Handle the click of the rotate buttons.
        """
        if self.file_path and os.path.isfile(self.file_path):
            background_tasks.create(self.apply_rotation(left))
        else:
            self.notify_problem("No file path configured for rotation.")

    async def apply_crop(self):
        try:
            path = self.file_path
            x = self.crop_x
            y = self.crop_y
            w = self.crop_width
            h = self.crop_height

            if not os.path.exists(path):
                self.notify_problem(f"Image not found at {path}")
                return

            with Image.open(path) as img:
                # Ensure we are working with correct coordinates
                box = (x, y, x + w, y + h)
                cropped_img = img.crop(box)

                # Save back to specific path (overwrite)
                # We save specifically to maintain format
                cropped_img.save(path)
                msg = f"{path} cropped to {w}x{h}"
                with self.container:
                    ui.notify(msg)
                with self.interactive_view:
                    self.interactive_view.force_reload()
        except Exception as ex:
            self.solution.handle_exception(ex)

    async def apply_rotation(self, left: bool):
        """
        Rotates the image at self.file_path by 90 degrees and saves it.

        Args:
            left: If True, rotate 90 degrees Counter-Clockwise.
                  If False, rotate 90 degrees Clockwise.
        """
        try:
            path = self.file_path
            if not os.path.exists(path):
                self.notify_problem(f"Image not found at {path}")
                return
                # Determine rotation angle constant
            # ROTATE_90 is Counter-Clockwise (Left)
            # ROTATE_270 is Counter-Clockwise 270 aka Clockwise 90 (Right)
            method = Image.Transpose.ROTATE_90 if left else Image.Transpose.ROTATE_270

            with Image.open(path) as img:
                rotated_img = img.transpose(method)
                rotated_img.save(path)

            direction = "left" if left else "right"
            msg = f"Rotated {direction} 90°"
            with self.container:
                ui.notify(msg)
                # Reset crop markup as dimensions/orientation have changed
            self.reset_crop()

            # Force browser to reload the image
            with self.interactive_view:
                self.interactive_view.force_reload()
        except Exception as ex:
            self.solution.handle_exception(ex)

apply_rotation(left) async

Rotates the image at self.file_path by 90 degrees and saves it.

Parameters:

Name Type Description Default
left bool

If True, rotate 90 degrees Counter-Clockwise. If False, rotate 90 degrees Clockwise.

required
Source code in ngwidgets/image_cropper.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
async def apply_rotation(self, left: bool):
    """
    Rotates the image at self.file_path by 90 degrees and saves it.

    Args:
        left: If True, rotate 90 degrees Counter-Clockwise.
              If False, rotate 90 degrees Clockwise.
    """
    try:
        path = self.file_path
        if not os.path.exists(path):
            self.notify_problem(f"Image not found at {path}")
            return
            # Determine rotation angle constant
        # ROTATE_90 is Counter-Clockwise (Left)
        # ROTATE_270 is Counter-Clockwise 270 aka Clockwise 90 (Right)
        method = Image.Transpose.ROTATE_90 if left else Image.Transpose.ROTATE_270

        with Image.open(path) as img:
            rotated_img = img.transpose(method)
            rotated_img.save(path)

        direction = "left" if left else "right"
        msg = f"Rotated {direction} 90°"
        with self.container:
            ui.notify(msg)
            # Reset crop markup as dimensions/orientation have changed
        self.reset_crop()

        # Force browser to reload the image
        with self.interactive_view:
            self.interactive_view.force_reload()
    except Exception as ex:
        self.solution.handle_exception(ex)

handle_mouse(e)

Handles mouse events to draw a crop rectangle on the interactive image.

Source code in ngwidgets/image_cropper.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def handle_mouse(self, e: events.MouseEventArguments):
    """
    Handles mouse events to draw a crop rectangle on the interactive image.
    """
    if e.type == "mousedown":
        self.dragging = True
        self.start_x = e.image_x
        self.start_y = e.image_y

    elif e.type == "mousemove":
        if self.dragging:
            self.update_crop_selection(e.image_x, e.image_y)

    elif e.type == "mouseup":
        self.dragging = False
        self.update_crop_selection(e.image_x, e.image_y)

on_crop_click() async

handle the click of the crop button

Source code in ngwidgets/image_cropper.py
153
154
155
156
157
158
159
160
async def on_crop_click(self):
    """
    handle the click of the crop button
    """
    if self.file_path and os.path.isfile(self.file_path):
        background_tasks.create(self.apply_crop())
    else:
        self.notify_problem("No file path configured for cropping.")

on_rotate_click(left=True) async

Handle the click of the rotate buttons.

Source code in ngwidgets/image_cropper.py
162
163
164
165
166
167
168
169
async def on_rotate_click(self, left: bool = True):
    """
    Handle the click of the rotate buttons.
    """
    if self.file_path and os.path.isfile(self.file_path):
        background_tasks.create(self.apply_rotation(left))
    else:
        self.notify_problem("No file path configured for rotation.")

reset_crop()

Resets crop variables and clears SVG overlay.

Source code in ngwidgets/image_cropper.py
140
141
142
143
144
145
146
147
def reset_crop(self):
    """Resets crop variables and clears SVG overlay."""
    self.crop_x = 0
    self.crop_y = 0
    self.crop_width = 0
    self.crop_height = 0
    if self.interactive_view:
        self.interactive_view.content = ""

set_source(source)

Updates the image source URL.

Source code in ngwidgets/image_cropper.py
95
96
97
98
def set_source(self, source: str):
    """Updates the image source URL."""
    if self.interactive_view:
        self.interactive_view.set_source(source)

setup_ui(container, image_url)

Sets up the UI elements.

Parameters:

Name Type Description Default
container

The parent container

required
image_url str

The URL for the browser to display

required
Source code in ngwidgets/image_cropper.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def setup_ui(self, container, image_url: str):
    """
    Sets up the UI elements.

    Args:
        container: The parent container
        image_url: The URL for the browser to display
    """
    self.container = container

    # Coordinate display
    with ui.row().classes("items-center"):
        ui.label("Crop Area:")
        with ui.row():
            ui.number(label="X", value=0).bind_value(self, "crop_x").props(
                "size=5 readonly"
            )
            ui.number(label="Y", value=0).bind_value(self, "crop_y").props(
                "size=5 readonly"
            )
            ui.number(label="W", value=0).bind_value(self, "crop_width").props(
                "size=5 readonly"
            )
            ui.number(label="H", value=0).bind_value(self, "crop_height").props(
                "size=5 readonly"
            )

        # Reset button convenient to have nearby
        ui.button(icon="crop_free", on_click=self.reset_crop).props(
            "flat round color=warning"
        ).tooltip("Reset Crop")
        # Crop button
        # Visibility is bound to crop_width. It appears only when width > 0.
        ui.button(icon="check", on_click=self.on_crop_click).props(
            "flat round color=positive"
        ).tooltip("Apply Crop").bind_visibility_from(
            self, "crop_width", backward=lambda w: w > 0
        )

        # Rotation buttons
        ui.button(
            icon="rotate_left", on_click=lambda: self.on_rotate_click(left=True)
        ).props("flat round color=primary").tooltip("Rotate Left 90°")
        ui.button(
            icon="rotate_right", on_click=lambda: self.on_rotate_click(left=False)
        ).props("flat round color=primary").tooltip("Rotate Right 90°")

    # Interactive Image implementation
    self.interactive_view = ui.interactive_image(
        image_url,
        on_mouse=self.handle_mouse,
        events=["mousedown", "mousemove", "mouseup"],
        cross=True,
    ).classes("w-full")

update_crop_selection(current_x, current_y)

Calculates the rectangle coordinates based on start and current pointers, updates state variables, and draws the SVG overlay.

Source code in ngwidgets/image_cropper.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def update_crop_selection(self, current_x: float, current_y: float):
    """
    Calculates the rectangle coordinates based on start and current pointers,
    updates state variables, and draws the SVG overlay.
    """
    # Calculate top-left corner and dimensions
    x = min(self.start_x, current_x)
    y = min(self.start_y, current_y)
    w = abs(self.start_x - current_x)
    h = abs(self.start_y - current_y)

    # Update persistent crop variables (convert to int for PIL)
    self.crop_x = int(x)
    self.crop_y = int(y)
    self.crop_width = int(w)
    self.crop_height = int(h)

    # Update the visual SVG overlay
    self.interactive_view.content = (
        f'<rect x="{x}" y="{y}" width="{w}" height="{h}" '
        f'fill="none" stroke="red" stroke-width="2" />'
    )

input_webserver

Created on 2023-09-12

@author: wf

InputWebSolution

Bases: WebSolution

A WebSolution that is focused on handling a single input.

Attributes:

Name Type Description
is_local bool

Indicates if the input source is local or remote.

input str

The input data or path to be processed.

Source code in ngwidgets/input_webserver.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class InputWebSolution(WebSolution):
    """
    A WebSolution that is focused on handling a single input.

    Attributes:
        is_local (bool): Indicates if the input source is local or remote.
        input (str): The input data or path to be processed.
    """

    def __init__(self, webserver: NiceGuiWebserver, client: Client):
        """
        Initializes the InputWebSolution instance with the webserver and client.

        Args:
            webserver (NiceGuiWebserver): The webserver instance this solution is part of.
            client (Client): The client interacting with this solution.
        """
        super().__init__(webserver, client)
        self.debug = webserver.debug
        self.root_path = None
        self.is_local = False
        self.input = ""
        self.render_on_load = False

    def input_changed(self, cargs):
        """
        react on changed input
        """
        self.input = cargs.value
        pass

    def read_input(self, input_str: str):
        """Reads the given input and handles any exceptions.

        Args:
            input_str (str): The input string representing a URL or local file.
        """
        try:
            ui.notify(f"reading {input_str}")
            if self.log_view:
                self.log_view.clear()
            self.error_msg = None
        except BaseException as e:
            self.handle_exception(e, self.do_trace)

    async def read_and_optionally_render(self, input_str, with_render: bool = False):
        """
        Reads the given input and optionally renders the given input

        Args:
            input_str (str): The input string representing a URL or local file.
            with_render(bool): if True also render
        """
        self.input_input.set_value(input_str)
        self.read_input(input_str)
        if with_render or self.args.render_on_load:
            await self.render(None)

    async def reload_file(self):
        """
        reload the input file
        """
        input_str = self.input
        if not input_str:
            return
        if os.path.exists(input_str):
            input_str = os.path.abspath(input_str)
        allowed = self.is_local
        if not self.is_local:
            for allowed_url in self.allowed_urls:
                if input_str.startswith(allowed_url):
                    allowed = True
        if not allowed:
            ui.notify("only white listed URLs and Path inputs are allowed")
        else:
            await self.read_and_optionally_render(self.input, with_render=True)

    async def open_file(self) -> None:
        """
        Opens a Local filer picker dialog and reads the
        selected input file."""
        if self.is_local:
            pick_list = await LocalFilePicker("~", multiple=False)
            if pick_list and len(pick_list) > 0:
                input_file = pick_list[0]
                with_render = self.render_on_load
                await self.read_and_optionally_render(
                    input_file, with_render=with_render
                )

    pass

    async def home(self):
        """
        provide the main content page

        """
        await self.setup_content_div(setup_content=None)

    async def about(self):
        """
        show about
        """
        await self.setup_content_div(self.setup_about)

    def setup_about(self):
        """
        display an About
        """
        self.about_div = About(self.config.version)

    async def setup_footer(
        self,
        with_log: bool = True,
        handle_logging: bool = True,
        max_lines: int = 20,
        log_classes: str = "w-full h-40",
    ):
        """
        setup a footer with an optional log view
        """
        if with_log:
            self.log_view = LogView(max_lines=max_lines, classes=log_classes)
            if handle_logging:
                self.log_view.addAsHandler(self.logger)
        else:
            self.log_view = None
        await super().setup_footer()
        if self.args.input:
            # await client.connected()
            with_render = self.render_on_load
            await self.read_and_optionally_render(
                self.args.input, with_render=with_render
            )

    async def settings(self):
        """
        Generates the settings page
        """

        def show():
            with ui.row():
                ui.checkbox("debug", value=self.webserver.debug).bind_value(
                    self.webserver, "debug"
                )
                ui.checkbox(
                    "debug with trace", value=self.webserver.do_trace
                ).bind_value(self.webserver, "do_trace")
                ui.checkbox("render on load", value=self.render_on_load).bind_value(
                    self, "render_on_load"
                )
            self.configure_settings()

        await self.setup_content_div(show)

    def configure_settings(self):
        """
        Configures settings specific to this web solution.
        This method is intended to be overridden by subclasses to provide custom settings behavior.
        The base method does nothing and can be extended in subclasses.
        """

    def configure_menu(self):
        """
        Configures the menu items specific to this web solution.
        This method is intended to be overridden by subclasses to provide custom menu behavior.
        The base method does nothing and can be extended in subclasses.
        """
        pass

    def prepare_ui(self):
        """
        handle the command line arguments
        """
        WebSolution.prepare_ui(self)
        args = self.webserver.args
        self.input = args.input
        self.root_path = self.webserver.root_path
        self.is_local = args.local
        self.render_on_load = args.render_on_load

__init__(webserver, client)

Initializes the InputWebSolution instance with the webserver and client.

Parameters:

Name Type Description Default
webserver NiceGuiWebserver

The webserver instance this solution is part of.

required
client Client

The client interacting with this solution.

required
Source code in ngwidgets/input_webserver.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(self, webserver: NiceGuiWebserver, client: Client):
    """
    Initializes the InputWebSolution instance with the webserver and client.

    Args:
        webserver (NiceGuiWebserver): The webserver instance this solution is part of.
        client (Client): The client interacting with this solution.
    """
    super().__init__(webserver, client)
    self.debug = webserver.debug
    self.root_path = None
    self.is_local = False
    self.input = ""
    self.render_on_load = False

about() async

show about

Source code in ngwidgets/input_webserver.py
152
153
154
155
156
async def about(self):
    """
    show about
    """
    await self.setup_content_div(self.setup_about)

configure_menu()

Configures the menu items specific to this web solution. This method is intended to be overridden by subclasses to provide custom menu behavior. The base method does nothing and can be extended in subclasses.

Source code in ngwidgets/input_webserver.py
215
216
217
218
219
220
221
def configure_menu(self):
    """
    Configures the menu items specific to this web solution.
    This method is intended to be overridden by subclasses to provide custom menu behavior.
    The base method does nothing and can be extended in subclasses.
    """
    pass

configure_settings()

Configures settings specific to this web solution. This method is intended to be overridden by subclasses to provide custom settings behavior. The base method does nothing and can be extended in subclasses.

Source code in ngwidgets/input_webserver.py
208
209
210
211
212
213
def configure_settings(self):
    """
    Configures settings specific to this web solution.
    This method is intended to be overridden by subclasses to provide custom settings behavior.
    The base method does nothing and can be extended in subclasses.
    """

home() async

provide the main content page

Source code in ngwidgets/input_webserver.py
145
146
147
148
149
150
async def home(self):
    """
    provide the main content page

    """
    await self.setup_content_div(setup_content=None)

input_changed(cargs)

react on changed input

Source code in ngwidgets/input_webserver.py
77
78
79
80
81
82
def input_changed(self, cargs):
    """
    react on changed input
    """
    self.input = cargs.value
    pass

open_file() async

Opens a Local filer picker dialog and reads the selected input file.

Source code in ngwidgets/input_webserver.py
130
131
132
133
134
135
136
137
138
139
140
141
async def open_file(self) -> None:
    """
    Opens a Local filer picker dialog and reads the
    selected input file."""
    if self.is_local:
        pick_list = await LocalFilePicker("~", multiple=False)
        if pick_list and len(pick_list) > 0:
            input_file = pick_list[0]
            with_render = self.render_on_load
            await self.read_and_optionally_render(
                input_file, with_render=with_render
            )

prepare_ui()

handle the command line arguments

Source code in ngwidgets/input_webserver.py
223
224
225
226
227
228
229
230
231
232
def prepare_ui(self):
    """
    handle the command line arguments
    """
    WebSolution.prepare_ui(self)
    args = self.webserver.args
    self.input = args.input
    self.root_path = self.webserver.root_path
    self.is_local = args.local
    self.render_on_load = args.render_on_load

read_and_optionally_render(input_str, with_render=False) async

Reads the given input and optionally renders the given input

Parameters:

Name Type Description Default
input_str str

The input string representing a URL or local file.

required
with_render bool

if True also render

False
Source code in ngwidgets/input_webserver.py
 98
 99
100
101
102
103
104
105
106
107
108
109
async def read_and_optionally_render(self, input_str, with_render: bool = False):
    """
    Reads the given input and optionally renders the given input

    Args:
        input_str (str): The input string representing a URL or local file.
        with_render(bool): if True also render
    """
    self.input_input.set_value(input_str)
    self.read_input(input_str)
    if with_render or self.args.render_on_load:
        await self.render(None)

read_input(input_str)

Reads the given input and handles any exceptions.

Parameters:

Name Type Description Default
input_str str

The input string representing a URL or local file.

required
Source code in ngwidgets/input_webserver.py
84
85
86
87
88
89
90
91
92
93
94
95
96
def read_input(self, input_str: str):
    """Reads the given input and handles any exceptions.

    Args:
        input_str (str): The input string representing a URL or local file.
    """
    try:
        ui.notify(f"reading {input_str}")
        if self.log_view:
            self.log_view.clear()
        self.error_msg = None
    except BaseException as e:
        self.handle_exception(e, self.do_trace)

reload_file() async

reload the input file

Source code in ngwidgets/input_webserver.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
async def reload_file(self):
    """
    reload the input file
    """
    input_str = self.input
    if not input_str:
        return
    if os.path.exists(input_str):
        input_str = os.path.abspath(input_str)
    allowed = self.is_local
    if not self.is_local:
        for allowed_url in self.allowed_urls:
            if input_str.startswith(allowed_url):
                allowed = True
    if not allowed:
        ui.notify("only white listed URLs and Path inputs are allowed")
    else:
        await self.read_and_optionally_render(self.input, with_render=True)

settings() async

Generates the settings page

Source code in ngwidgets/input_webserver.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
async def settings(self):
    """
    Generates the settings page
    """

    def show():
        with ui.row():
            ui.checkbox("debug", value=self.webserver.debug).bind_value(
                self.webserver, "debug"
            )
            ui.checkbox(
                "debug with trace", value=self.webserver.do_trace
            ).bind_value(self.webserver, "do_trace")
            ui.checkbox("render on load", value=self.render_on_load).bind_value(
                self, "render_on_load"
            )
        self.configure_settings()

    await self.setup_content_div(show)

setup_about()

display an About

Source code in ngwidgets/input_webserver.py
158
159
160
161
162
def setup_about(self):
    """
    display an About
    """
    self.about_div = About(self.config.version)

setup a footer with an optional log view

Source code in ngwidgets/input_webserver.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
async def setup_footer(
    self,
    with_log: bool = True,
    handle_logging: bool = True,
    max_lines: int = 20,
    log_classes: str = "w-full h-40",
):
    """
    setup a footer with an optional log view
    """
    if with_log:
        self.log_view = LogView(max_lines=max_lines, classes=log_classes)
        if handle_logging:
            self.log_view.addAsHandler(self.logger)
    else:
        self.log_view = None
    await super().setup_footer()
    if self.args.input:
        # await client.connected()
        with_render = self.render_on_load
        await self.read_and_optionally_render(
            self.args.input, with_render=with_render
        )

InputWebserver

Bases: NiceGuiWebserver

a webserver around a single input file of given filetypes

Source code in ngwidgets/input_webserver.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class InputWebserver(NiceGuiWebserver):
    """
    a webserver around a single input file of given filetypes
    """

    def __init__(self, config: WebserverConfig = None):
        """
        constructor
        """
        NiceGuiWebserver.__init__(self, config=config)

        @ui.page("/")
        async def home_page(client: Client):
            return await self.page(client, InputWebSolution.home)

        @ui.page("/settings")
        async def settings_page(client: Client):
            return await self.page(client, InputWebSolution.settings)

        @ui.page("/about")
        async def about_page(client: Client):
            return await self.page(client, InputWebSolution.about)

    def configure_run(self):
        """
        override the run configuration
        """
        NiceGuiWebserver.configure_run(self)
        args = self.args
        self.is_local = args.local
        if hasattr(args, "root_path"):
            self.root_path = os.path.abspath(args.root_path)
        else:
            self.root_path = None

__init__(config=None)

constructor

Source code in ngwidgets/input_webserver.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def __init__(self, config: WebserverConfig = None):
    """
    constructor
    """
    NiceGuiWebserver.__init__(self, config=config)

    @ui.page("/")
    async def home_page(client: Client):
        return await self.page(client, InputWebSolution.home)

    @ui.page("/settings")
    async def settings_page(client: Client):
        return await self.page(client, InputWebSolution.settings)

    @ui.page("/about")
    async def about_page(client: Client):
        return await self.page(client, InputWebSolution.about)

configure_run()

override the run configuration

Source code in ngwidgets/input_webserver.py
40
41
42
43
44
45
46
47
48
49
50
def configure_run(self):
    """
    override the run configuration
    """
    NiceGuiWebserver.configure_run(self)
    args = self.args
    self.is_local = args.local
    if hasattr(args, "root_path"):
        self.root_path = os.path.abspath(args.root_path)
    else:
        self.root_path = None

leaflet_map

Created on 2024-12-07

@author: wf

LeafletMap

Bases: leaflet

Enhanced Leaflet map with additional functionality

Source code in ngwidgets/leaflet_map.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class LeafletMap(ui.leaflet):
    """Enhanced Leaflet map with additional functionality"""

    def __init__(
        self,
        center: Tuple[float, float] = (51.505, -0.09),
        zoom: int = 9,
        with_draw_control: bool = True,
        classes: str = "w-full h-96",
        debug: bool = False,
    ):
        """Initialize enhanced map

        Args:
            center: Initial center coordinates as (lat,lon) tuple
            zoom: Initial zoom level
            with_draw_control: Whether to add the draw control
            classes: CSS classes to apply (default: h-96)
        """
        draw_control = (
            {
                "draw": {
                    "polygon": True,
                    "marker": True,
                    "circle": True,
                    "rectangle": True,
                    "polyline": True,
                    "circlemarker": True,
                },
                "edit": {
                    "edit": True,
                    "remove": True,
                },
            }
            if with_draw_control
            else None
        )

        super().__init__(center=center, zoom=zoom, draw_control=draw_control)
        self.debug = debug
        self.classes(classes)
        self.on("map-click", self._handle_click)
        self.on("draw:created", self._handle_draw)

    def _handle_click(self, e: events.GenericEventArguments):
        """Default click handler that adds a marker"""
        lat = e.args["latlng"]["lat"]
        lng = e.args["latlng"]["lng"]
        _marker = self.marker(latlng=(lat, lng))

    def _handle_draw(self, e: events.GenericEventArguments):
        """Default draw handler"""
        layer_type = e.args["layerType"]
        coords = e.args["layer"].get("_latlng") or e.args["layer"].get("_latlngs")
        layer_id = e.args["layer"].get("_leaflet_id")
        msg = f"Drawn a {layer_type} with id {layer_id} at {coords}"
        if self.debug:
            logging.warn(msg)
        ui.notify(msg)

    def draw_path(self, path: List[Tuple[float, float]], options: Dict = None) -> Any:
        """Draw a polyline path on the map
        Args:
            path: List of (lat,lon) coordinate tuples
            options: Optional styling options for the polyline
        Returns:
            The created layer object
        """
        if options is None:
            options = {"color": "red", "weight": 3, "opacity": 0.7}

        layer = self.generic_layer(name="polyline", args=[path, options])
        return layer

    def draw_tour(self, tour: Tour, leg_styles: LegStyles) -> List[Any]:
        """
        Draw a complete tour on the map with styled legs

        Args:
            tour: Tour to draw
            leg_styles: Style configuration for different leg types

        Returns:
            List of created layer objects
        """
        layers = []
        for leg in tour.legs:
            style = leg_styles.get_style(leg.leg_type)
            path = [leg.start.coordinates, leg.end.coordinates]
            options = {
                "color": style.color,
                "weight": style.weight,
                "opacity": style.opacity,
            }
            if style.dashArray:
                options["dashArray"] = style.dashArray
            layer = self.draw_path(path, options)
            layers.append(layer)
        return layers

__init__(center=(51.505, -0.09), zoom=9, with_draw_control=True, classes='w-full h-96', debug=False)

Initialize enhanced map

Parameters:

Name Type Description Default
center Tuple[float, float]

Initial center coordinates as (lat,lon) tuple

(51.505, -0.09)
zoom int

Initial zoom level

9
with_draw_control bool

Whether to add the draw control

True
classes str

CSS classes to apply (default: h-96)

'w-full h-96'
Source code in ngwidgets/leaflet_map.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def __init__(
    self,
    center: Tuple[float, float] = (51.505, -0.09),
    zoom: int = 9,
    with_draw_control: bool = True,
    classes: str = "w-full h-96",
    debug: bool = False,
):
    """Initialize enhanced map

    Args:
        center: Initial center coordinates as (lat,lon) tuple
        zoom: Initial zoom level
        with_draw_control: Whether to add the draw control
        classes: CSS classes to apply (default: h-96)
    """
    draw_control = (
        {
            "draw": {
                "polygon": True,
                "marker": True,
                "circle": True,
                "rectangle": True,
                "polyline": True,
                "circlemarker": True,
            },
            "edit": {
                "edit": True,
                "remove": True,
            },
        }
        if with_draw_control
        else None
    )

    super().__init__(center=center, zoom=zoom, draw_control=draw_control)
    self.debug = debug
    self.classes(classes)
    self.on("map-click", self._handle_click)
    self.on("draw:created", self._handle_draw)

draw_path(path, options=None)

Draw a polyline path on the map Args: path: List of (lat,lon) coordinate tuples options: Optional styling options for the polyline Returns: The created layer object

Source code in ngwidgets/leaflet_map.py
75
76
77
78
79
80
81
82
83
84
85
86
87
def draw_path(self, path: List[Tuple[float, float]], options: Dict = None) -> Any:
    """Draw a polyline path on the map
    Args:
        path: List of (lat,lon) coordinate tuples
        options: Optional styling options for the polyline
    Returns:
        The created layer object
    """
    if options is None:
        options = {"color": "red", "weight": 3, "opacity": 0.7}

    layer = self.generic_layer(name="polyline", args=[path, options])
    return layer

draw_tour(tour, leg_styles)

Draw a complete tour on the map with styled legs

Parameters:

Name Type Description Default
tour Tour

Tour to draw

required
leg_styles LegStyles

Style configuration for different leg types

required

Returns:

Type Description
List[Any]

List of created layer objects

Source code in ngwidgets/leaflet_map.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def draw_tour(self, tour: Tour, leg_styles: LegStyles) -> List[Any]:
    """
    Draw a complete tour on the map with styled legs

    Args:
        tour: Tour to draw
        leg_styles: Style configuration for different leg types

    Returns:
        List of created layer objects
    """
    layers = []
    for leg in tour.legs:
        style = leg_styles.get_style(leg.leg_type)
        path = [leg.start.coordinates, leg.end.coordinates]
        options = {
            "color": style.color,
            "weight": style.weight,
            "opacity": style.opacity,
        }
        if style.dashArray:
            options["dashArray"] = style.dashArray
        layer = self.draw_path(path, options)
        layers.append(layer)
    return layers

llm

Created on 2023-06-23 Updated on 2025-11-26 to support openrouter and clean up structure

@author: wf

LLM

Bases: BaseLLM

Large Language Model access wrapper via openai-python library. Supports OpenAI and OpenRouter (via base_url).

Source code in ngwidgets/llm.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
class LLM(BaseLLM):
    """
    Large Language Model access wrapper via openai-python library.
    Supports OpenAI and OpenRouter (via base_url).
    """

    # Define a dictionary that maps models to their size limits
    MODEL_SIZE_LIMITS = {
        "gpt-3.5-turbo": 4096,
        "gpt-4o-mini": 128000,
        "google/gemini-2.0-flash-001": 1000000,
        "anthropic/claude-3.5-sonnet": 200000,
    }

    # Define a constant for the average token character length
    AVERAGE_TOKEN_CHAR_LEN = 4
    DEFAULT_MODEL = "gpt-4o-mini"

    def __init__(
        self,
        api_key: str = None,
        model: str = DEFAULT_MODEL,
        force_key: bool = False,
        prompts_filepath: str = None,
        base_url: str = None,
    ):
        """
        constructor

        Args:
            api_key(str): the access key
            model(str): the model to use
            force_key(bool): if True, raise ValueError if no key is available
            prompts_filepath(str): the filepath for the prompt logging
            base_url(str): custom base URL (e.g. https://openrouter.ai/api/v1)
        """
        self.client = None
        self.model = model
        self.token_size_limit = LLM.MODEL_SIZE_LIMITS.get(model, 4096)
        self.char_size_limit = self.token_size_limit * LLM.AVERAGE_TOKEN_CHAR_LEN
        self.base_url = base_url

        # Resolve API Key
        self.api_key = self.get_api_key(api_key)

        if self.api_key is None:
            if force_key:
                raise ValueError(
                    "No API key found. Please set 'OPENAI_API_KEY' (or 'OPENROUTER_API_KEY') or check config files."
                )
            else:
                return

        # Initialize Client
        self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)

        # Setup Prompts Logging
        self.prompts_filepath = self._get_prompts_filepath(prompts_filepath)

        if self.prompts_filepath and self.prompts_filepath.is_file():
            self.prompts = Prompts.load_from_yaml_file(
                str(self.prompts_filepath)
            )  # @UndefinedVariable
        else:
            self.prompts = Prompts()

    def _is_openrouter(self) -> bool:
        """Check if current configuration is pointing to OpenRouter"""
        return self.base_url and "openrouter" in self.base_url

    def get_api_key(self, api_key: str) -> str:
        """
        Resolve the API key from arguments, environment variables, or configuration files.
        Prioritizes OpenRouter sources if base_url indicates OpenRouter usage.
        """
        if api_key:
            return api_key

        if self._is_openrouter():
            return self._get_openrouter_key()

        return self._get_openai_key()

    def _get_openrouter_key(self) -> str:
        """
        Retrieve OpenRouter API key from environment or config file.
        """
        # Check Env
        key = os.getenv("OPENROUTER_API_KEY")
        if key:
            return key

        # Check File
        key_file = Path.home() / ".openrouter" / "apikey.txt"
        if key_file.is_file():
            return key_file.read_text().strip()

        return None

    def _get_openai_key(self) -> str:
        """
        Retrieve Standard OpenAI API key from environment or config file.
        """
        # Check Env
        key = os.getenv("OPENAI_API_KEY")
        if key:
            return key

        # Check File
        json_file = Path.home() / ".openai" / "openai_api_key.json"
        if json_file.is_file():
            try:
                with open(json_file, "r") as file:
                    data = json.load(file)
                    return data.get("OPENAI_API_KEY")
            except Exception:
                pass
        return None

    def _get_prompts_filepath(self, filepath_arg: str) -> Path:
        """
        Determine the file path for logging prompts.
        """
        if filepath_arg:
            return Path(filepath_arg)

        date_str = datetime.now().strftime("%Y-%m-%d")
        default_filename = f"prompts_{date_str}.yaml"
        openai_dir = Path.home() / ".openai"
        if not openai_dir.exists():
            openai_dir.mkdir(parents=True, exist_ok=True)
        return openai_dir / default_filename

    def _get_extra_headers(self) -> dict:
        """Get extra headers for OpenRouter if applicable"""
        if self._is_openrouter():
            return {
                "HTTP-Referer": "https://github.com/WolfgangFahl/ngwidgets",
                "X-Title": "ngwidgets",
            }
        return None

    def available(self):
        """
        check availability of API by making sure there is an api_key
        """
        return self.client is not None

    def ask(self, prompt_text: str, model: str = None, temperature: float = 0.7) -> str:
        """
        ask a prompt
        """
        if len(prompt_text) > self.char_size_limit:
            raise ValueError(
                f"Prompt exceeds size limit of {self.char_size_limit} characters."
            )
        if model is None:
            model = self.model

        start_time = datetime.now()

        chat_completion = self.client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt_text}],
            temperature=temperature,
            extra_headers=self._get_extra_headers(),
        )
        result = chat_completion.choices[0].message.content
        new_prompt = Prompt.from_chat(
            prompt_text,
            result,
            chat_completion,
            start_time,
            model,
            temperature=temperature,
        )
        self.prompts.add_to_filepath(new_prompt, self.prompts_filepath)
        return result

__init__(api_key=None, model=DEFAULT_MODEL, force_key=False, prompts_filepath=None, base_url=None)

constructor

Parameters:

Name Type Description Default
api_key str

the access key

None
model str

the model to use

DEFAULT_MODEL
force_key bool

if True, raise ValueError if no key is available

False
prompts_filepath str

the filepath for the prompt logging

None
base_url str

custom base URL (e.g. https://openrouter.ai/api/v1)

None
Source code in ngwidgets/llm.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def __init__(
    self,
    api_key: str = None,
    model: str = DEFAULT_MODEL,
    force_key: bool = False,
    prompts_filepath: str = None,
    base_url: str = None,
):
    """
    constructor

    Args:
        api_key(str): the access key
        model(str): the model to use
        force_key(bool): if True, raise ValueError if no key is available
        prompts_filepath(str): the filepath for the prompt logging
        base_url(str): custom base URL (e.g. https://openrouter.ai/api/v1)
    """
    self.client = None
    self.model = model
    self.token_size_limit = LLM.MODEL_SIZE_LIMITS.get(model, 4096)
    self.char_size_limit = self.token_size_limit * LLM.AVERAGE_TOKEN_CHAR_LEN
    self.base_url = base_url

    # Resolve API Key
    self.api_key = self.get_api_key(api_key)

    if self.api_key is None:
        if force_key:
            raise ValueError(
                "No API key found. Please set 'OPENAI_API_KEY' (or 'OPENROUTER_API_KEY') or check config files."
            )
        else:
            return

    # Initialize Client
    self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)

    # Setup Prompts Logging
    self.prompts_filepath = self._get_prompts_filepath(prompts_filepath)

    if self.prompts_filepath and self.prompts_filepath.is_file():
        self.prompts = Prompts.load_from_yaml_file(
            str(self.prompts_filepath)
        )  # @UndefinedVariable
    else:
        self.prompts = Prompts()

ask(prompt_text, model=None, temperature=0.7)

ask a prompt

Source code in ngwidgets/llm.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def ask(self, prompt_text: str, model: str = None, temperature: float = 0.7) -> str:
    """
    ask a prompt
    """
    if len(prompt_text) > self.char_size_limit:
        raise ValueError(
            f"Prompt exceeds size limit of {self.char_size_limit} characters."
        )
    if model is None:
        model = self.model

    start_time = datetime.now()

    chat_completion = self.client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt_text}],
        temperature=temperature,
        extra_headers=self._get_extra_headers(),
    )
    result = chat_completion.choices[0].message.content
    new_prompt = Prompt.from_chat(
        prompt_text,
        result,
        chat_completion,
        start_time,
        model,
        temperature=temperature,
    )
    self.prompts.add_to_filepath(new_prompt, self.prompts_filepath)
    return result

available()

check availability of API by making sure there is an api_key

Source code in ngwidgets/llm.py
161
162
163
164
165
def available(self):
    """
    check availability of API by making sure there is an api_key
    """
    return self.client is not None

get_api_key(api_key)

Resolve the API key from arguments, environment variables, or configuration files. Prioritizes OpenRouter sources if base_url indicates OpenRouter usage.

Source code in ngwidgets/llm.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def get_api_key(self, api_key: str) -> str:
    """
    Resolve the API key from arguments, environment variables, or configuration files.
    Prioritizes OpenRouter sources if base_url indicates OpenRouter usage.
    """
    if api_key:
        return api_key

    if self._is_openrouter():
        return self._get_openrouter_key()

    return self._get_openai_key()

VisionLLM

Bases: LLM

Extension of LLM class to handle image analysis

Source code in ngwidgets/llm.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
class VisionLLM(LLM):
    """
    Extension of LLM class to handle image analysis
    """

    def __init__(
        self,
        api_key: str = None,
        model="gpt-4o-mini",
        force_key: bool = False,
        base_url: str = None,
    ):
        super().__init__(
            api_key=api_key, model=model, force_key=force_key, base_url=base_url
        )

    def analyze_image(
        self, image_path: str, prompt_text: str, auth: dict = None
    ) -> str:
        """
        Analyze an image with a given prompt
        """
        start_time = datetime.now()
        if image_path.startswith("http"):
            if auth:
                url_parts = image_path.split("://", 1)
                image_url = f"{url_parts[0]}://{auth['username']}:{auth['password']}@{url_parts[1]}"
            else:
                image_url = image_path
            image_content = {"type": "image_url", "image_url": {"url": image_url}}
        else:
            if not os.path.exists(image_path):
                return f"Error: Image path '{image_path}' does not exist."

            with open(image_path, "rb") as image_file:
                image_base64 = base64.b64encode(image_file.read()).decode("utf-8")
                image_content = {
                    "type": "image_url",
                    "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"},
                }

        messages = [
            {
                "role": "user",
                "content": [{"type": "text", "text": prompt_text}, image_content],
            }
        ]

        chat_completion = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            extra_headers=self._get_extra_headers(),
        )

        result = chat_completion.choices[0].message.content
        new_prompt = Prompt.from_chat(
            prompt_text,
            result,
            chat_completion,
            start_time,
            self.model,
            image_path=image_path,
        )
        self.prompts.add_to_filepath(new_prompt, self.prompts_filepath)

        return result

analyze_image(image_path, prompt_text, auth=None)

Analyze an image with a given prompt

Source code in ngwidgets/llm.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def analyze_image(
    self, image_path: str, prompt_text: str, auth: dict = None
) -> str:
    """
    Analyze an image with a given prompt
    """
    start_time = datetime.now()
    if image_path.startswith("http"):
        if auth:
            url_parts = image_path.split("://", 1)
            image_url = f"{url_parts[0]}://{auth['username']}:{auth['password']}@{url_parts[1]}"
        else:
            image_url = image_path
        image_content = {"type": "image_url", "image_url": {"url": image_url}}
    else:
        if not os.path.exists(image_path):
            return f"Error: Image path '{image_path}' does not exist."

        with open(image_path, "rb") as image_file:
            image_base64 = base64.b64encode(image_file.read()).decode("utf-8")
            image_content = {
                "type": "image_url",
                "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"},
            }

    messages = [
        {
            "role": "user",
            "content": [{"type": "text", "text": prompt_text}, image_content],
        }
    ]

    chat_completion = self.client.chat.completions.create(
        model=self.model,
        messages=messages,
        extra_headers=self._get_extra_headers(),
    )

    result = chat_completion.choices[0].message.content
    new_prompt = Prompt.from_chat(
        prompt_text,
        result,
        chat_completion,
        start_time,
        self.model,
        image_path=image_path,
    )
    self.prompts.add_to_filepath(new_prompt, self.prompts_filepath)

    return result

local_filepicker

LocalFilePicker

Bases: dialog

see https://raw.githubusercontent.com/zauberzeug/nicegui/main/examples/local_file_picker/local_file_picker.py

Comments improved by Claude AI

A simple file picker that allows selection of files from the local filesystem where NiceGUI is running.

Attributes:

Name Type Description
path Path

The current directory path.

upper_limit Optional[Path]

The upper limit directory.

show_hidden_files bool

Whether to show hidden files.

grid aggrid

The AgGrid component for displaying files.

drives_toggle Optional[toggle]

Toggle for selecting drives on Windows.

Parameters:

Name Type Description Default
directory str

The directory to start in.

required
upper_limit Optional[str]

The directory to stop at. Defaults to the starting directory.

...
multiple bool

Whether to allow multiple file selection. Defaults to False.

False
show_hidden_files bool

Whether to show hidden files. Defaults to False.

False
Source code in ngwidgets/local_filepicker.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
class LocalFilePicker(ui.dialog):
    """
    see
    https://raw.githubusercontent.com/zauberzeug/nicegui/main/examples/local_file_picker/local_file_picker.py

    Comments improved by Claude AI

    A simple file picker that allows selection of files from the local filesystem where NiceGUI is running.

    Attributes:
        path (Path): The current directory path.
        upper_limit (Optional[Path]): The upper limit directory.
        show_hidden_files (bool): Whether to show hidden files.
        grid (ui.aggrid): The AgGrid component for displaying files.
        drives_toggle (Optional[ui.toggle]): Toggle for selecting drives on Windows.

    Args:
        directory (str): The directory to start in.
        upper_limit (Optional[str]): The directory to stop at. Defaults to the starting directory.
        multiple (bool): Whether to allow multiple file selection. Defaults to False.
        show_hidden_files (bool): Whether to show hidden files. Defaults to False.
    """

    def __init__(
        self,
        directory: str,
        *,
        upper_limit: Optional[str] = ...,
        multiple: bool = False,
        show_hidden_files: bool = False,
    ) -> None:
        super().__init__()

        self.path: Path = Path(directory).expanduser()
        self.upper_limit: Optional[Path] = (
            None
            if upper_limit is None
            else Path(directory if upper_limit == ... else upper_limit).expanduser()
        )
        self.show_hidden_files: bool = show_hidden_files
        self.grid: ui.aggrid
        self.drives_toggle: Optional[ui.toggle] = None

        with self, ui.card():
            self.add_drives_toggle()
            self.grid = (
                ui.aggrid(
                    {
                        "columnDefs": [{"field": "name", "headerName": "File"}],
                        "rowSelection": "multiple" if multiple else "single",
                    },
                    html_columns=[0],
                )
                .classes("w-96")
                .on("cellDoubleClicked", self.handle_double_click)
            )
            with ui.row().classes("w-full justify-end"):
                ui.button("Cancel", on_click=self.close).props("outline")
                ui.button("Ok", on_click=self._handle_ok)
        self.update_grid()

    def add_drives_toggle(self) -> None:
        """
        Adds a toggle for selecting drives on Windows platforms.
        """
        if platform.system() == "Windows":
            import win32api

            drives: List[str] = win32api.GetLogicalDriveStrings().split("\000")[:-1]
            self.drives_toggle = ui.toggle(
                drives, value=drives[0], on_change=self.update_drive
            )

    def update_drive(self) -> None:
        """
        Updates the current path when a new drive is selected.
        """
        if self.drives_toggle:
            self.path = Path(self.drives_toggle.value).expanduser()
            self.update_grid()

    def update_grid(self) -> None:
        """
        Updates the grid with the current directory contents.
        """
        paths: List[Path] = list(self.path.glob("*"))
        if not self.show_hidden_files:
            paths = [p for p in paths if not p.name.startswith(".")]
        paths.sort(key=lambda p: p.name.lower())
        paths.sort(key=lambda p: not p.is_dir())

        self.grid.options["rowData"] = [
            {
                "name": f"📁 <strong>{p.name}</strong>" if p.is_dir() else p.name,
                "path": str(p),
            }
            for p in paths
        ]
        if (self.upper_limit is None and self.path != self.path.parent) or (
            self.upper_limit is not None and self.path != self.upper_limit
        ):
            self.grid.options["rowData"].insert(
                0,
                {
                    "name": "📁 <strong>..</strong>",
                    "path": str(self.path.parent),
                },
            )
        self.grid.update()

    def handle_double_click(self, e: events.GenericEventArguments) -> None:
        """
        Handles double-click events on grid items.

        Args:
            e (events.GenericEventArguments): The event arguments.
        """
        self.path = Path(e.args["data"]["path"])
        if self.path.is_dir():
            self.update_grid()
        else:
            self.submit([str(self.path)])

    async def _handle_ok(self) -> None:
        """
        Handles the 'Ok' button click event.
        """
        rows: List[dict] = await self.grid.get_selected_rows()
        self.submit([r["path"] for r in rows])

add_drives_toggle()

Adds a toggle for selecting drives on Windows platforms.

Source code in ngwidgets/local_filepicker.py
69
70
71
72
73
74
75
76
77
78
79
def add_drives_toggle(self) -> None:
    """
    Adds a toggle for selecting drives on Windows platforms.
    """
    if platform.system() == "Windows":
        import win32api

        drives: List[str] = win32api.GetLogicalDriveStrings().split("\000")[:-1]
        self.drives_toggle = ui.toggle(
            drives, value=drives[0], on_change=self.update_drive
        )

handle_double_click(e)

Handles double-click events on grid items.

Parameters:

Name Type Description Default
e GenericEventArguments

The event arguments.

required
Source code in ngwidgets/local_filepicker.py
118
119
120
121
122
123
124
125
126
127
128
129
def handle_double_click(self, e: events.GenericEventArguments) -> None:
    """
    Handles double-click events on grid items.

    Args:
        e (events.GenericEventArguments): The event arguments.
    """
    self.path = Path(e.args["data"]["path"])
    if self.path.is_dir():
        self.update_grid()
    else:
        self.submit([str(self.path)])

update_drive()

Updates the current path when a new drive is selected.

Source code in ngwidgets/local_filepicker.py
81
82
83
84
85
86
87
def update_drive(self) -> None:
    """
    Updates the current path when a new drive is selected.
    """
    if self.drives_toggle:
        self.path = Path(self.drives_toggle.value).expanduser()
        self.update_grid()

update_grid()

Updates the grid with the current directory contents.

Source code in ngwidgets/local_filepicker.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def update_grid(self) -> None:
    """
    Updates the grid with the current directory contents.
    """
    paths: List[Path] = list(self.path.glob("*"))
    if not self.show_hidden_files:
        paths = [p for p in paths if not p.name.startswith(".")]
    paths.sort(key=lambda p: p.name.lower())
    paths.sort(key=lambda p: not p.is_dir())

    self.grid.options["rowData"] = [
        {
            "name": f"📁 <strong>{p.name}</strong>" if p.is_dir() else p.name,
            "path": str(p),
        }
        for p in paths
    ]
    if (self.upper_limit is None and self.path != self.path.parent) or (
        self.upper_limit is not None and self.path != self.upper_limit
    ):
        self.grid.options["rowData"].insert(
            0,
            {
                "name": "📁 <strong>..</strong>",
                "path": str(self.path.parent),
            },
        )
    self.grid.update()

lod_grid

Created on 2023-10-3

@author: wf

GridConfig dataclass

Configuration for initializing a ListOfDictsGrid.

Source code in ngwidgets/lod_grid.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@dataclass
class GridConfig:
    """
    Configuration for initializing a ListOfDictsGrid.
    """

    key_col: str = "#"
    column_defs: Optional[List[Dict]] = None
    options: Dict = field(default_factory=dict)
    # optics
    theme: str = "material"
    classes: str = "h-screen overflow-auto"
    all_cols_html: bool = True
    # behavior
    lenient: bool = False
    # default column defs
    autoHeight: bool = True
    sortable: bool = True
    resizable: bool = True
    editable: bool = False
    wrapText: bool = True
    # row options
    multiselect: bool = False
    auto_size_columns: bool = True
    # buttons
    with_buttons: bool = False
    button_names: List[str] = field(
        default_factory=lambda: ["new", "delete", "all", "fit"]
    )
    prepend_new: bool = True
    html_columns: List[int] = field(default_factory=list)
    keygen_callback: Optional[Callable] = None
    exception_callback: Optional[Callable] = None
    debug: bool = False

ListOfDictsGrid

ag grid based on list of dict

see https://nicegui.io/documentation/ag_grid see https://github.com/zauberzeug/nicegui/discussions/1833

Source code in ngwidgets/lod_grid.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
class ListOfDictsGrid:
    """
    ag grid based on list of dict

    see https://nicegui.io/documentation/ag_grid
    see https://github.com/zauberzeug/nicegui/discussions/1833
    """

    def __init__(
        self, lod: Optional[List[Dict]] = None, config: GridConfig = None
    ) -> None:
        """
        Initialize the ListOfDictsGrid object.

        Args:
            lod (Optional[List[Dict]]): List of dictionaries to be displayed.
            config(GridConfig): configuration for the grid behavior
        """
        self.lod = lod
        self.config = config or GridConfig()
        self.lod_index = {}
        self.all_selected = False  # track selection state
        try:
            if self.config.with_buttons:
                self.setup_button_row(self.config.button_names)
            # Update options to include onGridReady event handling
            self.config.options[":onGridReady"] = (
                "(params) => params.columnApi.autoSizeAllColumns()"
            )

            self.ag_grid = ui.aggrid(
                options=self.config.options,
                html_columns=self.config.html_columns,
            ).classes(self.config.classes)
            self.ag_grid.theme = self.config.theme
            self.auto_size_columns = self.config.auto_size_columns
            self.setDefaultColDef()
            if lod is not None:
                self.load_lod(lod, self.config.column_defs)
        except Exception as ex:
            self.handle_exception(ex)

    @property
    def options(self):
        return self.ag_grid._props.get("options", {})

    @options.setter
    def options(self, value):
        self.ag_grid._props["options"] = value

    @property
    def html_columns(self):
        return self.ag_grid._props.get("html_columns", [])

    @html_columns.setter
    def html_columns(self, value):
        self.ag_grid._props["html_columns"] = value

    @property
    def auto_size_columns(self):
        return self.ag_grid._props.get("auto_size_columns", True)

    @auto_size_columns.setter
    def auto_size_columns(self, value):
        self.ag_grid._props["auto_size_columns"] = value

    def get_column_def(self, col: str) -> Dict:
        """
        get the column definition for the given column

        Args:
            col (str): The field name of the column where checkboxes should be enabled.

        Returns:
            Dict: the column definition
        """
        if not self.ag_grid.options.get("columnDefs"):
            raise Exception(
                "Column definitions are not set. Load the data first using load_lod."
            )
        # Go through each column definition
        for col_def in self.ag_grid.options["columnDefs"]:
            if col_def["field"] == col:
                return col_def
        return None

    def set_column_def(self, col: str, key: str, value: Any) -> Dict:
        """
        Set a value in a column definition dictionary for a specified column.

        This method updates the column definition dictionary for a given column by
        setting a specific key to a provided value. If the column definition exists,
        the key-value pair is updated; if not, no changes are made.

        Parameters:
            col (str): The name of the column to update.
            key (str): The key in the column definition dictionary to set.
            value (Any): The value to assign to the key in the dictionary.

        Returns:
            Dict: The updated column definition dictionary, or None if the column does not exist.
        """
        col_def = self.get_column_def(
            col
        )  # Assuming get_column_def is defined elsewhere.
        if col_def:
            col_def[key] = value
        return col_def

    def set_checkbox_renderer(self, checkbox_col: str):
        """
        set cellRenderer to checkBoxRenderer for the given column

        Args:
            checkbox_col (str): The field name of the column where
            rendering as checkboxes should be enabled.

        """
        col_def = self.get_column_def(checkbox_col)
        col_def["cellRenderer"] = "checkboxRenderer"

    def set_checkbox_selection(self, checkbox_col: str):
        """
        Set the checkbox selection for a specified column.

        Args:
            checkbox_col (str): The field name of the column where checkboxes should be enabled.
        """
        col_def = self.get_column_def(checkbox_col)
        if col_def:
            col_def["checkboxSelection"] = True

    def handle_exception(self, ex: Exception) -> None:
        """
        Handles exceptions thrown during grid initialization or operation.

        In debug mode, this method prints the stack trace and re-raises the exception for further debugging. In non-debug mode, it notifies the user of a general error.

        Args:
            ex (Exception): The exception that was caught.

        Raises:
            Exception: Re-raises the exception in debug mode for further debugging.
        """
        error_msg = f"Exception caught in ListOfDictsGrid: {str(ex)}"
        if self.config.debug:
            # Print a stack trace to stderr
            print(error_msg, file=sys.stderr)
            traceback.print_exc()
        if self.config.exception_callback:
            self.config.exception_callback(ex)
        else:
            logging.error(error_msg, exc_info=ex)
            pass

    def get_index(self, lenient: bool = False, lod=None, key_col: str = None):
        """
        get and index (dict of dicts) of the given list of
        dicts based on the given key column - if no key_col is given
        index by row number starting from 1
        """
        lod_index = {}
        if lod is None:
            lod = self.lod
        if lod:
            for row_index, row in enumerate(lod):
                if key_col is None:
                    lod_index[row_index + 1] = row
                else:
                    if self.config.key_col in row:
                        key_value = row[key_col]
                        lod_index[key_value] = row
                    else:
                        msg = f"missing key column {self.config.key_col} in row {row_index}"
                        if not lenient:
                            raise Exception(msg)
                        else:
                            print(msg, file=sys.stderr)
                        # missing key
                        pass
        return lod_index

    def update_index(self, lenient: bool = False):
        """
        update my index (with view records)
        """
        self.lod_index = self.get_index(
            lenient=lenient, lod=self.lod, key_col=self.config.key_col
        )

    def get_row_for_key(self, key_value: str):
        """
        the the row for the given key_value

        Args:
            key_value: str
        """
        row = self.lod_index.get(key_value, None)
        return row

    def get_cell_value(self, key_value: Any, col_key: str) -> Any:
        """
        get the value for the given cell

        Args:
            key_value (Any): The value of the key column for the row to update.
            row_key (str): The column key of the cell to update.

        Returns:
            Any: the value of the cell or None if the row doesn't exist
        """
        rows_by_key = self.get_rows_by_key()
        row = rows_by_key.get(key_value, None)
        value = None
        if row:
            value = row.get(col_key, None)
        return value

    def update_cell(self, key_value: Any, col_key: str, value: Any) -> None:
        """
        Update a cell in the grid.

        Args:
            key_value (Any): The value of the key column for the row to update.
            row_key (str): The column key of the cell to update.
            value (Any): The new value for the specified cell.

        """
        rows_by_key = self.get_rows_by_key()
        row = rows_by_key.get(key_value, None)
        if row:
            row[col_key] = value

    def get_row_data(self):
        """
        get the complete row data
        """
        row_data = self.ag_grid.options["rowData"]
        return row_data

    def get_rows_by_key(self) -> Dict[Any, Dict[str, Any]]:
        """
        Organize rows in a dictionary of dictionaries, indexed by the key column value specified in GridConfig.

        Returns:
            Dict[Any, Dict[str, Any]]: A dictionary of dictionaries, with each sub-dictionary representing a row,
                                       indexed by the key column values.
        """
        data_by_key = {}
        key_col = (
            self.config.key_col
        )  # Retrieve key column name from the GridConfig instance
        for row in self.get_row_data():
            key_value = row.get(key_col, None)
            if key_value is not None:
                data_by_key[key_value] = row
        return data_by_key

    async def onSizeColumnsToFit(self, _msg: dict):
        """
        see https://www.reddit.com/r/nicegui/comments/17cg0o5/aggrid_autosize_columns_to_data_width/
        """
        # await asyncio.sleep(0.2)
        self.sizeColumnsToFit()

    def sizeColumnsToFit(self):
        if self.ag_grid:
            self.ag_grid.run_grid_method("autoSizeAllColumns")
            self.ag_grid.update()

    def setDefaultColDef(self):
        """
        set the default column definitions
        """
        if not "defaultColDef" in self.ag_grid.options:
            self.ag_grid.options["defaultColDef"] = {}
        if self.config.multiselect:
            # Apply settings for multiple row selection
            self.ag_grid.options["rowSelection"] = "multiple"
        defaultColDef = self.ag_grid.options["defaultColDef"]
        defaultColDef["resizable"] = self.config.resizable
        defaultColDef["sortable"] = self.config.sortable
        # https://www.ag-grid.com/javascript-data-grid/grid-size/
        defaultColDef["wrapText"] = self.config.wrapText
        defaultColDef["autoHeight"] = self.config.autoHeight
        defaultColDef["editable"] = self.config.editable

    def load_lod(self, lod: list, columnDefs: list = None):
        """
        load the given list of dicts

        Args:
            lod(list): a list of dicts to be loaded into the grid
            columnDefs(list): a list of column definitions
        """
        try:
            if columnDefs is None:
                # assume lod
                columnDefs = []
                if len(lod) > 0:
                    header = lod[0]
                    for key, value in header.items():
                        if isinstance(value, int) or isinstance(value, float):
                            col_filter = "agNumberColumnFilter"
                        elif isinstance(value, datetime.datetime) or isinstance(
                            value, datetime.date
                        ):
                            col_filter = "agDateColumnFilter"
                        else:
                            col_filter = True  # Use default filter
                        columnDefs.append(dict({"field": key, "filter": col_filter}))
            self.ag_grid.options["columnDefs"] = columnDefs
            self.ag_grid.options["rowData"] = lod
            self.update_index(lenient=self.config.lenient)
            if self.config.all_cols_html:
                # Set html_columns based on all_rows_html flag
                html_columns = list(range(len(columnDefs)))
                self.html_columns = html_columns
        except Exception as ex:
            self.handle_exception(ex)

    def update(self):
        """
        update my aggrid
        """
        if self.ag_grid:
            self.ag_grid.update()

    async def get_selected_rows(self):
        """
        get the currently selected rows
        """
        selected_rows = await self.ag_grid.get_selected_rows()
        return selected_rows

    def select_rows_by_keys(self, key_values: List[Any]) -> None:
        """
        Select rows in the grid based on a list of key values.

        Args:
            key_values (List[Any]): List of key values to be selected.
        """
        row_data = self.get_row_data()
        for row_index, row in enumerate(row_data):
            key_value = row.get(self.config.key_col)
            if key_value in key_values:
                self.ag_grid.run_row_method(row_index, "setSelected", True)

    async def get_selected_lod(self, lod_index=None):
        """
        selected rows are in view (e.g. potentially  html) format
        get back the original list of dict rows
        """
        selected_lod = []
        selected_rows = await self.get_selected_rows()
        if lod_index is None:
            lod_index = self.get_index(lenient=self.config.lenient, lod=self.lod)
        for row in selected_rows:
            key_value = row[self.config.key_col]
            record = lod_index[key_value]
            selected_lod.append(record)
        return selected_lod

    def select_all_rows(self):
        """
        select all my ag_grid rows
        """
        self.ag_grid.run_grid_method("selectAll")

    def deselect_all_rows(self):
        """
        deselect all my ag_grid rows
        see https://stackoverflow.com/a/52199985/1497139
        """
        self.ag_grid.run_grid_method("deselectAll")

    def toggle_select_all_rows(self):
        """
        Toggle between selecting all rows and deselecting all rows.
        """
        if self.all_selected:
            self.deselect_all_rows()
        else:
            self.select_all_rows()
        self.all_selected = not self.all_selected
        self.update_select_all_toggle_button()

    def update_select_all_toggle_button(self):
        """
        Update the toggle button's label and icon based on selection state.
        see https://github.com/zauberzeug/nicegui/discussions/3596
        """
        stb = self.select_toggle_button
        if self.all_selected:
            stb.text = "None"
            stb.props("icon=check_box_outline_blank")
        else:
            stb.text = "All"
            stb.props("icon=select_all")
        stb.update()

    async def delete_selected_rows(self, _args):
        """
        Delete the currently selected rows based on the key column.
        """
        # Await the asynchronous call to get selected rows
        selected_rows = await self.get_selected_rows()
        if len(selected_rows) == 0:
            ui.notify("no rows selected for delete", type="warning")
            return
        # Get the list of keys of selected rows
        selected_keys = [row[self.config.key_col] for row in selected_rows]
        # Notify the user about the operation
        ui.notify(f"deleting rows with keys {selected_keys}")
        # Update the data to exclude selected rows
        self.lod[:] = [
            row for row in self.lod if row[self.config.key_col] not in selected_keys
        ]
        # Update the grid to reflect changes
        self.update()

    async def new_row(self, _args):
        """
        add a new row
        """
        try:
            # Handle the key column
            if (
                self.config.key_col == "#"
            ):  # If the key column is '#' treating it as an integer index
                new_key = len(self.lod)
            elif (
                self.config.keygen_callback
            ):  # If a key generation callback is provided
                new_key = self.config.keygen_callback()
            else:  # If the key column isn't '#' and no keygen callback is provided
                msg = f"Missing keygen_callback to create new key for '{self.config.key_col}' column"
                ui.notify(msg, type="negative")
                return
            ui.notify(f"new row with {self.config.key_col}={new_key}")
            new_record = {f"{self.config.key_col}": new_key}
            if self.config.prepend_new:
                self.lod.insert(0, new_record)
            else:
                self.lod.append(new_record)
            self.update()
        except Exception as ex:
            self.handle_exception(ex)

    def setup_button_row(self, button_names: list):
        """
        set up a button row

        Args:
            button_names (list): the list of buttons to be setup
        """
        with ui.row() as self.button_row:
            # icons per https://fonts.google.com/icons
            if self.config.editable:
                if "new" in button_names:
                    self.new_button = ui.button(
                        "New", icon="add", on_click=self.new_row
                    )
                if "delete" in button_names:
                    self.delete_button = ui.button(
                        "Delete", icon="delete", on_click=self.delete_selected_rows
                    )
            if "fit" in button_names:
                self.fit_button = ui.button(
                    "Fit", icon="fit_screen", on_click=self.onSizeColumnsToFit
                )
            if "all" in button_names:
                self.select_toggle_button = ui.button(
                    "All",
                    icon="select_all",
                    on_click=self.toggle_select_all_rows,
                )

__init__(lod=None, config=None)

Initialize the ListOfDictsGrid object.

Parameters:

Name Type Description Default
lod Optional[List[Dict]]

List of dictionaries to be displayed.

None
config GridConfig

configuration for the grid behavior

None
Source code in ngwidgets/lod_grid.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def __init__(
    self, lod: Optional[List[Dict]] = None, config: GridConfig = None
) -> None:
    """
    Initialize the ListOfDictsGrid object.

    Args:
        lod (Optional[List[Dict]]): List of dictionaries to be displayed.
        config(GridConfig): configuration for the grid behavior
    """
    self.lod = lod
    self.config = config or GridConfig()
    self.lod_index = {}
    self.all_selected = False  # track selection state
    try:
        if self.config.with_buttons:
            self.setup_button_row(self.config.button_names)
        # Update options to include onGridReady event handling
        self.config.options[":onGridReady"] = (
            "(params) => params.columnApi.autoSizeAllColumns()"
        )

        self.ag_grid = ui.aggrid(
            options=self.config.options,
            html_columns=self.config.html_columns,
        ).classes(self.config.classes)
        self.ag_grid.theme = self.config.theme
        self.auto_size_columns = self.config.auto_size_columns
        self.setDefaultColDef()
        if lod is not None:
            self.load_lod(lod, self.config.column_defs)
    except Exception as ex:
        self.handle_exception(ex)

delete_selected_rows(_args) async

Delete the currently selected rows based on the key column.

Source code in ngwidgets/lod_grid.py
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
async def delete_selected_rows(self, _args):
    """
    Delete the currently selected rows based on the key column.
    """
    # Await the asynchronous call to get selected rows
    selected_rows = await self.get_selected_rows()
    if len(selected_rows) == 0:
        ui.notify("no rows selected for delete", type="warning")
        return
    # Get the list of keys of selected rows
    selected_keys = [row[self.config.key_col] for row in selected_rows]
    # Notify the user about the operation
    ui.notify(f"deleting rows with keys {selected_keys}")
    # Update the data to exclude selected rows
    self.lod[:] = [
        row for row in self.lod if row[self.config.key_col] not in selected_keys
    ]
    # Update the grid to reflect changes
    self.update()

deselect_all_rows()

deselect all my ag_grid rows see https://stackoverflow.com/a/52199985/1497139

Source code in ngwidgets/lod_grid.py
422
423
424
425
426
427
def deselect_all_rows(self):
    """
    deselect all my ag_grid rows
    see https://stackoverflow.com/a/52199985/1497139
    """
    self.ag_grid.run_grid_method("deselectAll")

get_cell_value(key_value, col_key)

get the value for the given cell

Parameters:

Name Type Description Default
key_value Any

The value of the key column for the row to update.

required
row_key str

The column key of the cell to update.

required

Returns:

Name Type Description
Any Any

the value of the cell or None if the row doesn't exist

Source code in ngwidgets/lod_grid.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def get_cell_value(self, key_value: Any, col_key: str) -> Any:
    """
    get the value for the given cell

    Args:
        key_value (Any): The value of the key column for the row to update.
        row_key (str): The column key of the cell to update.

    Returns:
        Any: the value of the cell or None if the row doesn't exist
    """
    rows_by_key = self.get_rows_by_key()
    row = rows_by_key.get(key_value, None)
    value = None
    if row:
        value = row.get(col_key, None)
    return value

get_column_def(col)

get the column definition for the given column

Parameters:

Name Type Description Default
col str

The field name of the column where checkboxes should be enabled.

required

Returns:

Name Type Description
Dict Dict

the column definition

Source code in ngwidgets/lod_grid.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def get_column_def(self, col: str) -> Dict:
    """
    get the column definition for the given column

    Args:
        col (str): The field name of the column where checkboxes should be enabled.

    Returns:
        Dict: the column definition
    """
    if not self.ag_grid.options.get("columnDefs"):
        raise Exception(
            "Column definitions are not set. Load the data first using load_lod."
        )
    # Go through each column definition
    for col_def in self.ag_grid.options["columnDefs"]:
        if col_def["field"] == col:
            return col_def
    return None

get_index(lenient=False, lod=None, key_col=None)

get and index (dict of dicts) of the given list of dicts based on the given key column - if no key_col is given index by row number starting from 1

Source code in ngwidgets/lod_grid.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def get_index(self, lenient: bool = False, lod=None, key_col: str = None):
    """
    get and index (dict of dicts) of the given list of
    dicts based on the given key column - if no key_col is given
    index by row number starting from 1
    """
    lod_index = {}
    if lod is None:
        lod = self.lod
    if lod:
        for row_index, row in enumerate(lod):
            if key_col is None:
                lod_index[row_index + 1] = row
            else:
                if self.config.key_col in row:
                    key_value = row[key_col]
                    lod_index[key_value] = row
                else:
                    msg = f"missing key column {self.config.key_col} in row {row_index}"
                    if not lenient:
                        raise Exception(msg)
                    else:
                        print(msg, file=sys.stderr)
                    # missing key
                    pass
    return lod_index

get_row_data()

get the complete row data

Source code in ngwidgets/lod_grid.py
286
287
288
289
290
291
def get_row_data(self):
    """
    get the complete row data
    """
    row_data = self.ag_grid.options["rowData"]
    return row_data

get_row_for_key(key_value)

the the row for the given key_value

Parameters:

Name Type Description Default
key_value str

str

required
Source code in ngwidgets/lod_grid.py
243
244
245
246
247
248
249
250
251
def get_row_for_key(self, key_value: str):
    """
    the the row for the given key_value

    Args:
        key_value: str
    """
    row = self.lod_index.get(key_value, None)
    return row

get_rows_by_key()

Organize rows in a dictionary of dictionaries, indexed by the key column value specified in GridConfig.

Returns:

Type Description
Dict[Any, Dict[str, Any]]

Dict[Any, Dict[str, Any]]: A dictionary of dictionaries, with each sub-dictionary representing a row, indexed by the key column values.

Source code in ngwidgets/lod_grid.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def get_rows_by_key(self) -> Dict[Any, Dict[str, Any]]:
    """
    Organize rows in a dictionary of dictionaries, indexed by the key column value specified in GridConfig.

    Returns:
        Dict[Any, Dict[str, Any]]: A dictionary of dictionaries, with each sub-dictionary representing a row,
                                   indexed by the key column values.
    """
    data_by_key = {}
    key_col = (
        self.config.key_col
    )  # Retrieve key column name from the GridConfig instance
    for row in self.get_row_data():
        key_value = row.get(key_col, None)
        if key_value is not None:
            data_by_key[key_value] = row
    return data_by_key

get_selected_lod(lod_index=None) async

selected rows are in view (e.g. potentially html) format get back the original list of dict rows

Source code in ngwidgets/lod_grid.py
401
402
403
404
405
406
407
408
409
410
411
412
413
414
async def get_selected_lod(self, lod_index=None):
    """
    selected rows are in view (e.g. potentially  html) format
    get back the original list of dict rows
    """
    selected_lod = []
    selected_rows = await self.get_selected_rows()
    if lod_index is None:
        lod_index = self.get_index(lenient=self.config.lenient, lod=self.lod)
    for row in selected_rows:
        key_value = row[self.config.key_col]
        record = lod_index[key_value]
        selected_lod.append(record)
    return selected_lod

get_selected_rows() async

get the currently selected rows

Source code in ngwidgets/lod_grid.py
381
382
383
384
385
386
async def get_selected_rows(self):
    """
    get the currently selected rows
    """
    selected_rows = await self.ag_grid.get_selected_rows()
    return selected_rows

handle_exception(ex)

Handles exceptions thrown during grid initialization or operation.

In debug mode, this method prints the stack trace and re-raises the exception for further debugging. In non-debug mode, it notifies the user of a general error.

Parameters:

Name Type Description Default
ex Exception

The exception that was caught.

required

Raises:

Type Description
Exception

Re-raises the exception in debug mode for further debugging.

Source code in ngwidgets/lod_grid.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def handle_exception(self, ex: Exception) -> None:
    """
    Handles exceptions thrown during grid initialization or operation.

    In debug mode, this method prints the stack trace and re-raises the exception for further debugging. In non-debug mode, it notifies the user of a general error.

    Args:
        ex (Exception): The exception that was caught.

    Raises:
        Exception: Re-raises the exception in debug mode for further debugging.
    """
    error_msg = f"Exception caught in ListOfDictsGrid: {str(ex)}"
    if self.config.debug:
        # Print a stack trace to stderr
        print(error_msg, file=sys.stderr)
        traceback.print_exc()
    if self.config.exception_callback:
        self.config.exception_callback(ex)
    else:
        logging.error(error_msg, exc_info=ex)
        pass

load_lod(lod, columnDefs=None)

load the given list of dicts

Parameters:

Name Type Description Default
lod list

a list of dicts to be loaded into the grid

required
columnDefs list

a list of column definitions

None
Source code in ngwidgets/lod_grid.py
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def load_lod(self, lod: list, columnDefs: list = None):
    """
    load the given list of dicts

    Args:
        lod(list): a list of dicts to be loaded into the grid
        columnDefs(list): a list of column definitions
    """
    try:
        if columnDefs is None:
            # assume lod
            columnDefs = []
            if len(lod) > 0:
                header = lod[0]
                for key, value in header.items():
                    if isinstance(value, int) or isinstance(value, float):
                        col_filter = "agNumberColumnFilter"
                    elif isinstance(value, datetime.datetime) or isinstance(
                        value, datetime.date
                    ):
                        col_filter = "agDateColumnFilter"
                    else:
                        col_filter = True  # Use default filter
                    columnDefs.append(dict({"field": key, "filter": col_filter}))
        self.ag_grid.options["columnDefs"] = columnDefs
        self.ag_grid.options["rowData"] = lod
        self.update_index(lenient=self.config.lenient)
        if self.config.all_cols_html:
            # Set html_columns based on all_rows_html flag
            html_columns = list(range(len(columnDefs)))
            self.html_columns = html_columns
    except Exception as ex:
        self.handle_exception(ex)

new_row(_args) async

add a new row

Source code in ngwidgets/lod_grid.py
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
async def new_row(self, _args):
    """
    add a new row
    """
    try:
        # Handle the key column
        if (
            self.config.key_col == "#"
        ):  # If the key column is '#' treating it as an integer index
            new_key = len(self.lod)
        elif (
            self.config.keygen_callback
        ):  # If a key generation callback is provided
            new_key = self.config.keygen_callback()
        else:  # If the key column isn't '#' and no keygen callback is provided
            msg = f"Missing keygen_callback to create new key for '{self.config.key_col}' column"
            ui.notify(msg, type="negative")
            return
        ui.notify(f"new row with {self.config.key_col}={new_key}")
        new_record = {f"{self.config.key_col}": new_key}
        if self.config.prepend_new:
            self.lod.insert(0, new_record)
        else:
            self.lod.append(new_record)
        self.update()
    except Exception as ex:
        self.handle_exception(ex)

onSizeColumnsToFit(_msg) async

see https://www.reddit.com/r/nicegui/comments/17cg0o5/aggrid_autosize_columns_to_data_width/

Source code in ngwidgets/lod_grid.py
311
312
313
314
315
316
async def onSizeColumnsToFit(self, _msg: dict):
    """
    see https://www.reddit.com/r/nicegui/comments/17cg0o5/aggrid_autosize_columns_to_data_width/
    """
    # await asyncio.sleep(0.2)
    self.sizeColumnsToFit()

select_all_rows()

select all my ag_grid rows

Source code in ngwidgets/lod_grid.py
416
417
418
419
420
def select_all_rows(self):
    """
    select all my ag_grid rows
    """
    self.ag_grid.run_grid_method("selectAll")

select_rows_by_keys(key_values)

Select rows in the grid based on a list of key values.

Parameters:

Name Type Description Default
key_values List[Any]

List of key values to be selected.

required
Source code in ngwidgets/lod_grid.py
388
389
390
391
392
393
394
395
396
397
398
399
def select_rows_by_keys(self, key_values: List[Any]) -> None:
    """
    Select rows in the grid based on a list of key values.

    Args:
        key_values (List[Any]): List of key values to be selected.
    """
    row_data = self.get_row_data()
    for row_index, row in enumerate(row_data):
        key_value = row.get(self.config.key_col)
        if key_value in key_values:
            self.ag_grid.run_row_method(row_index, "setSelected", True)

setDefaultColDef()

set the default column definitions

Source code in ngwidgets/lod_grid.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def setDefaultColDef(self):
    """
    set the default column definitions
    """
    if not "defaultColDef" in self.ag_grid.options:
        self.ag_grid.options["defaultColDef"] = {}
    if self.config.multiselect:
        # Apply settings for multiple row selection
        self.ag_grid.options["rowSelection"] = "multiple"
    defaultColDef = self.ag_grid.options["defaultColDef"]
    defaultColDef["resizable"] = self.config.resizable
    defaultColDef["sortable"] = self.config.sortable
    # https://www.ag-grid.com/javascript-data-grid/grid-size/
    defaultColDef["wrapText"] = self.config.wrapText
    defaultColDef["autoHeight"] = self.config.autoHeight
    defaultColDef["editable"] = self.config.editable

set_checkbox_renderer(checkbox_col)

set cellRenderer to checkBoxRenderer for the given column

Parameters:

Name Type Description Default
checkbox_col str

The field name of the column where

required
Source code in ngwidgets/lod_grid.py
162
163
164
165
166
167
168
169
170
171
172
def set_checkbox_renderer(self, checkbox_col: str):
    """
    set cellRenderer to checkBoxRenderer for the given column

    Args:
        checkbox_col (str): The field name of the column where
        rendering as checkboxes should be enabled.

    """
    col_def = self.get_column_def(checkbox_col)
    col_def["cellRenderer"] = "checkboxRenderer"

set_checkbox_selection(checkbox_col)

Set the checkbox selection for a specified column.

Parameters:

Name Type Description Default
checkbox_col str

The field name of the column where checkboxes should be enabled.

required
Source code in ngwidgets/lod_grid.py
174
175
176
177
178
179
180
181
182
183
def set_checkbox_selection(self, checkbox_col: str):
    """
    Set the checkbox selection for a specified column.

    Args:
        checkbox_col (str): The field name of the column where checkboxes should be enabled.
    """
    col_def = self.get_column_def(checkbox_col)
    if col_def:
        col_def["checkboxSelection"] = True

set_column_def(col, key, value)

Set a value in a column definition dictionary for a specified column.

This method updates the column definition dictionary for a given column by setting a specific key to a provided value. If the column definition exists, the key-value pair is updated; if not, no changes are made.

Parameters:

Name Type Description Default
col str

The name of the column to update.

required
key str

The key in the column definition dictionary to set.

required
value Any

The value to assign to the key in the dictionary.

required

Returns:

Name Type Description
Dict Dict

The updated column definition dictionary, or None if the column does not exist.

Source code in ngwidgets/lod_grid.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def set_column_def(self, col: str, key: str, value: Any) -> Dict:
    """
    Set a value in a column definition dictionary for a specified column.

    This method updates the column definition dictionary for a given column by
    setting a specific key to a provided value. If the column definition exists,
    the key-value pair is updated; if not, no changes are made.

    Parameters:
        col (str): The name of the column to update.
        key (str): The key in the column definition dictionary to set.
        value (Any): The value to assign to the key in the dictionary.

    Returns:
        Dict: The updated column definition dictionary, or None if the column does not exist.
    """
    col_def = self.get_column_def(
        col
    )  # Assuming get_column_def is defined elsewhere.
    if col_def:
        col_def[key] = value
    return col_def

setup_button_row(button_names)

set up a button row

Parameters:

Name Type Description Default
button_names list

the list of buttons to be setup

required
Source code in ngwidgets/lod_grid.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
def setup_button_row(self, button_names: list):
    """
    set up a button row

    Args:
        button_names (list): the list of buttons to be setup
    """
    with ui.row() as self.button_row:
        # icons per https://fonts.google.com/icons
        if self.config.editable:
            if "new" in button_names:
                self.new_button = ui.button(
                    "New", icon="add", on_click=self.new_row
                )
            if "delete" in button_names:
                self.delete_button = ui.button(
                    "Delete", icon="delete", on_click=self.delete_selected_rows
                )
        if "fit" in button_names:
            self.fit_button = ui.button(
                "Fit", icon="fit_screen", on_click=self.onSizeColumnsToFit
            )
        if "all" in button_names:
            self.select_toggle_button = ui.button(
                "All",
                icon="select_all",
                on_click=self.toggle_select_all_rows,
            )

toggle_select_all_rows()

Toggle between selecting all rows and deselecting all rows.

Source code in ngwidgets/lod_grid.py
429
430
431
432
433
434
435
436
437
438
def toggle_select_all_rows(self):
    """
    Toggle between selecting all rows and deselecting all rows.
    """
    if self.all_selected:
        self.deselect_all_rows()
    else:
        self.select_all_rows()
    self.all_selected = not self.all_selected
    self.update_select_all_toggle_button()

update()

update my aggrid

Source code in ngwidgets/lod_grid.py
374
375
376
377
378
379
def update(self):
    """
    update my aggrid
    """
    if self.ag_grid:
        self.ag_grid.update()

update_cell(key_value, col_key, value)

Update a cell in the grid.

Parameters:

Name Type Description Default
key_value Any

The value of the key column for the row to update.

required
row_key str

The column key of the cell to update.

required
value Any

The new value for the specified cell.

required
Source code in ngwidgets/lod_grid.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def update_cell(self, key_value: Any, col_key: str, value: Any) -> None:
    """
    Update a cell in the grid.

    Args:
        key_value (Any): The value of the key column for the row to update.
        row_key (str): The column key of the cell to update.
        value (Any): The new value for the specified cell.

    """
    rows_by_key = self.get_rows_by_key()
    row = rows_by_key.get(key_value, None)
    if row:
        row[col_key] = value

update_index(lenient=False)

update my index (with view records)

Source code in ngwidgets/lod_grid.py
235
236
237
238
239
240
241
def update_index(self, lenient: bool = False):
    """
    update my index (with view records)
    """
    self.lod_index = self.get_index(
        lenient=lenient, lod=self.lod, key_col=self.config.key_col
    )

update_select_all_toggle_button()

Update the toggle button's label and icon based on selection state. see https://github.com/zauberzeug/nicegui/discussions/3596

Source code in ngwidgets/lod_grid.py
440
441
442
443
444
445
446
447
448
449
450
451
452
def update_select_all_toggle_button(self):
    """
    Update the toggle button's label and icon based on selection state.
    see https://github.com/zauberzeug/nicegui/discussions/3596
    """
    stb = self.select_toggle_button
    if self.all_selected:
        stb.text = "None"
        stb.props("icon=check_box_outline_blank")
    else:
        stb.text = "All"
        stb.props("icon=select_all")
    stb.update()

log_view

Created on 2023-11-15

A custom logging handler that emits messages to a NiceGUI log element and a standard logger.

@author: wf

LogView

Handle logging by delegating to a ui.log

Source code in ngwidgets/log_view.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
class LogView:
    """
    Handle logging by delegating to a ui.log
    """

    LOG_LEVEL_ICONS = {
        logging.DEBUG: "🐞",
        logging.INFO: "ℹ️",
        logging.WARNING: "⚠️",
        logging.ERROR: "❌",
        logging.CRITICAL: "🔥",
    }

    def __init__(
        self,
        max_lines: int = 100,
        classes: str = "w-full",
        level: int = logging.NOTSET,
        log_stream: TextIO = sys.stderr,
        with_sleep: float = None,
    ):
        self.level = level
        self.with_sleep = with_sleep
        self.loggers: List[logging.Logger] = []
        self.ui_log = ui.log(max_lines=max_lines).classes(classes)
        self.handler = UiLogHandler(self, level)
        self.fallback_handler = logging.StreamHandler(log_stream)
        self.fallback_handler.setLevel(logging.DEBUG)

    def on_fail(self, ex: Exception):
        self.ui_log = None
        self.handler.ui_log = None
        self.fallback_handler.emit(
            f"ui.log failure: {str(ex)} - switching off ui log ..."
        )

    def clear(self):
        """
        clear my ui_log
        """
        if self.ui_log is not None:
            try:
                self.ui_log.clear()
            except Exception as ex:
                self.on_fail(ex)

    def iconize(self, msg: str, level: int) -> str:
        level_icon = self.LOG_LEVEL_ICONS.get(level, "")
        if not msg.startswith(level_icon):
            msg = f"{level_icon}:{msg}"
        return msg

    def push(self, msg: str, level: int = None):
        """
        Push a message to the UI log.

        Args:
            msg (str): The message to push to the log.
        """
        if level is None:
            level = self.level
        msg = self.iconize(msg, level)
        if self.ui_log is None:
            self.fallback_handler.emit(
                logging.makeLogRecord({"msg": msg, "levelno": self.level})
            )
        else:
            try:
                self.ui_log.push(msg)
                if self.with_sleep:
                    time.sleep(self.with_sleep)
            except Exception as ex:
                self.on_fail(ex)

    def setLevel(self, level: int):
        """
        Set the logging level for this handler and all associated loggers.

        Args:
            level (int): The logging level to set.
        """
        self.level = level
        self.handler.setLevel(level)
        self.fallback_handler.setLevel(level)
        for logger in self.loggers:
            logger.setLevel(level)

    def addAsHandler(self, logger: logging.Logger):
        """
        Add this LogView as a handler to the given logger.

        Args:
            logger (logging.Logger): The logger to add this handler to.
        """
        if logger in self.loggers:
            return
        if self.handler not in logger.handlers:
            if self.handler.addAsHandler(logger):
                self.loggers.append(logger)

addAsHandler(logger)

Add this LogView as a handler to the given logger.

Parameters:

Name Type Description Default
logger Logger

The logger to add this handler to.

required
Source code in ngwidgets/log_view.py
105
106
107
108
109
110
111
112
113
114
115
116
def addAsHandler(self, logger: logging.Logger):
    """
    Add this LogView as a handler to the given logger.

    Args:
        logger (logging.Logger): The logger to add this handler to.
    """
    if logger in self.loggers:
        return
    if self.handler not in logger.handlers:
        if self.handler.addAsHandler(logger):
            self.loggers.append(logger)

clear()

clear my ui_log

Source code in ngwidgets/log_view.py
54
55
56
57
58
59
60
61
62
def clear(self):
    """
    clear my ui_log
    """
    if self.ui_log is not None:
        try:
            self.ui_log.clear()
        except Exception as ex:
            self.on_fail(ex)

push(msg, level=None)

Push a message to the UI log.

Parameters:

Name Type Description Default
msg str

The message to push to the log.

required
Source code in ngwidgets/log_view.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def push(self, msg: str, level: int = None):
    """
    Push a message to the UI log.

    Args:
        msg (str): The message to push to the log.
    """
    if level is None:
        level = self.level
    msg = self.iconize(msg, level)
    if self.ui_log is None:
        self.fallback_handler.emit(
            logging.makeLogRecord({"msg": msg, "levelno": self.level})
        )
    else:
        try:
            self.ui_log.push(msg)
            if self.with_sleep:
                time.sleep(self.with_sleep)
        except Exception as ex:
            self.on_fail(ex)

setLevel(level)

Set the logging level for this handler and all associated loggers.

Parameters:

Name Type Description Default
level int

The logging level to set.

required
Source code in ngwidgets/log_view.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def setLevel(self, level: int):
    """
    Set the logging level for this handler and all associated loggers.

    Args:
        level (int): The logging level to set.
    """
    self.level = level
    self.handler.setLevel(level)
    self.fallback_handler.setLevel(level)
    for logger in self.loggers:
        logger.setLevel(level)

UiLogHandler

Bases: Handler

A logging handler that emits messages to a NiceGUI log element. Uses a fallback logger to make sure messages do not get lost when the log element is not available

Source code in ngwidgets/log_view.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
class UiLogHandler(logging.Handler):
    """
    A logging handler that emits messages to a NiceGUI log element.
    Uses a fallback logger to make sure messages do not get lost when the log element is not available
    """

    def __init__(
        self,
        log_view: LogView,
        level: int = logging.NOTSET,
    ):
        """
        Constructor

        Args:
            log_view (LogView): the LogView which encapsultes the ui.log in which messages will be emitted.
            level (int): The logging level for this handler. Default is logging.NOTSET.
        """
        super().__init__(level)
        self.log_view = log_view

    def addAsHandler(self, logger: logging.Logger) -> bool:
        """
        Add this handler to the given logger if it's not already added.

        Args:
            logger (logging.Logger): The logger to add this handler to.

        Returns:
            bool: True if the handler was added, False if it was already present.
        """
        if self not in logger.handlers:
            logger.addHandler(self)
            # see https://nicegui.io/documentation/log#attach_to_a_logger
            ui.context.client.on_disconnect(lambda: logger.removeHandler(self))
            return True
        else:
            return False

    def emit(self, record: logging.LogRecord):
        """
        Emit a record to the NiceGUI log element
        and a fallback standard logging.

        Args:
            record (logging.LogRecord): The log record to be emitted.
        """
        try:
            formatted_msg = self.format(record)
            self.log_view.push(formatted_msg, record.levelno)
        except Exception as ex:
            self.log_view.on_fail(ex)

    def setLevel(self, level):
        """
        Set the logging level of this handler.

        Args:
            level (int): The logging level to set.
        """
        super().setLevel(level)

__init__(log_view, level=logging.NOTSET)

Constructor

Parameters:

Name Type Description Default
log_view LogView

the LogView which encapsultes the ui.log in which messages will be emitted.

required
level int

The logging level for this handler. Default is logging.NOTSET.

NOTSET
Source code in ngwidgets/log_view.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def __init__(
    self,
    log_view: LogView,
    level: int = logging.NOTSET,
):
    """
    Constructor

    Args:
        log_view (LogView): the LogView which encapsultes the ui.log in which messages will be emitted.
        level (int): The logging level for this handler. Default is logging.NOTSET.
    """
    super().__init__(level)
    self.log_view = log_view

addAsHandler(logger)

Add this handler to the given logger if it's not already added.

Parameters:

Name Type Description Default
logger Logger

The logger to add this handler to.

required

Returns:

Name Type Description
bool bool

True if the handler was added, False if it was already present.

Source code in ngwidgets/log_view.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def addAsHandler(self, logger: logging.Logger) -> bool:
    """
    Add this handler to the given logger if it's not already added.

    Args:
        logger (logging.Logger): The logger to add this handler to.

    Returns:
        bool: True if the handler was added, False if it was already present.
    """
    if self not in logger.handlers:
        logger.addHandler(self)
        # see https://nicegui.io/documentation/log#attach_to_a_logger
        ui.context.client.on_disconnect(lambda: logger.removeHandler(self))
        return True
    else:
        return False

emit(record)

Emit a record to the NiceGUI log element and a fallback standard logging.

Parameters:

Name Type Description Default
record LogRecord

The log record to be emitted.

required
Source code in ngwidgets/log_view.py
158
159
160
161
162
163
164
165
166
167
168
169
170
def emit(self, record: logging.LogRecord):
    """
    Emit a record to the NiceGUI log element
    and a fallback standard logging.

    Args:
        record (logging.LogRecord): The log record to be emitted.
    """
    try:
        formatted_msg = self.format(record)
        self.log_view.push(formatted_msg, record.levelno)
    except Exception as ex:
        self.log_view.on_fail(ex)

setLevel(level)

Set the logging level of this handler.

Parameters:

Name Type Description Default
level int

The logging level to set.

required
Source code in ngwidgets/log_view.py
172
173
174
175
176
177
178
179
def setLevel(self, level):
    """
    Set the logging level of this handler.

    Args:
        level (int): The logging level to set.
    """
    super().setLevel(level)

login

Created on 2023-10-31

@author: wf

Login

Bases: object

nicegui login support

Source code in ngwidgets/login.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Login(object):
    """
    nicegui login support
    """

    def __init__(self, webserver, users):
        """
        Constructor
        """
        self.webserver = webserver
        self.users = users

    def authenticated(self) -> bool:
        """
        check whether the current user is authenticated
        """
        result = app.storage.user.get("authenticated", False)
        return result

    async def logout(self):
        """
        logout
        """
        app.storage.user.update({"username": None, "authenticated": False})

    def get_username(self) -> str:
        """
        Get the username of the currently logged-in user
        """
        return app.storage.user.get("username", "?")

    async def login(self, solution):
        """
        login
        """
        # this might not work if there as already been HTML output
        if self.authenticated():
            return RedirectResponse("/")

        await solution.setup_content_div(self.show_login)

    async def show_login(self):
        """
        show the login view
        """

        def try_login() -> (
            None
        ):  # local function to avoid passing username and password as arguments
            if self.users.check_password(username.value, password.value):
                app.storage.user.update(
                    {"username": username.value, "authenticated": True}
                )
                ui.navigate.to("/")
            else:
                ui.notify("Wrong username or password", color="negative")

        with ui.card().classes("absolute-center"):
            username = ui.input("Username").on("keydown.enter", try_login)
            password = ui.input(
                "Password", password=True, password_toggle_button=True
            ).on("keydown.enter", try_login)
            ui.button("Log in", on_click=try_login)

__init__(webserver, users)

Constructor

Source code in ngwidgets/login.py
16
17
18
19
20
21
def __init__(self, webserver, users):
    """
    Constructor
    """
    self.webserver = webserver
    self.users = users

authenticated()

check whether the current user is authenticated

Source code in ngwidgets/login.py
23
24
25
26
27
28
def authenticated(self) -> bool:
    """
    check whether the current user is authenticated
    """
    result = app.storage.user.get("authenticated", False)
    return result

get_username()

Get the username of the currently logged-in user

Source code in ngwidgets/login.py
36
37
38
39
40
def get_username(self) -> str:
    """
    Get the username of the currently logged-in user
    """
    return app.storage.user.get("username", "?")

login(solution) async

login

Source code in ngwidgets/login.py
42
43
44
45
46
47
48
49
50
async def login(self, solution):
    """
    login
    """
    # this might not work if there as already been HTML output
    if self.authenticated():
        return RedirectResponse("/")

    await solution.setup_content_div(self.show_login)

logout() async

logout

Source code in ngwidgets/login.py
30
31
32
33
34
async def logout(self):
    """
    logout
    """
    app.storage.user.update({"username": None, "authenticated": False})

show_login() async

show the login view

Source code in ngwidgets/login.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
async def show_login(self):
    """
    show the login view
    """

    def try_login() -> (
        None
    ):  # local function to avoid passing username and password as arguments
        if self.users.check_password(username.value, password.value):
            app.storage.user.update(
                {"username": username.value, "authenticated": True}
            )
            ui.navigate.to("/")
        else:
            ui.notify("Wrong username or password", color="negative")

    with ui.card().classes("absolute-center"):
        username = ui.input("Username").on("keydown.enter", try_login)
        password = ui.input(
            "Password", password=True, password_toggle_button=True
        ).on("keydown.enter", try_login)
        ui.button("Log in", on_click=try_login)

markup_header

Created on 2023-11-19

@author: wf

MarkupHeader

Helper to generate tabulate compatible markup header lines.

Source code in ngwidgets/markup_header.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class MarkupHeader:
    """
    Helper to generate tabulate compatible markup header lines.
    """

    @classmethod
    def get_markup(cls, title: str, markup_format: str, level: int = 1) -> str:
        """
        Generates a formatted header string based on the specified markup format.

        Args:
            title (str): The title to be formatted as the header.
            markup_format (str): The markup format for the header.
            level (int): The section level to generate a header for.

        Returns:
            str: The formatted header string.
        """
        if markup_format == "github":
            return f"{'#' * level} {title}\n"
        elif markup_format == "mediawiki":
            return f"{'=' * level} {title} {'=' * level}\n"
        elif markup_format == "html" or markup_format == "unsafehtml":
            return f"<h{level}>{title}</h{level}>"
        elif markup_format == "latex":
            if level == 1:
                return f"\\section{{{title}}}"
            elif level == 2:
                return f"\\subsection{{{title}}}"
            elif level == 3:
                return f"\\subsubsection{{{title}}}"
        elif markup_format == "textile":
            return f"h{level}. {title}"
        elif markup_format == "plain":
            return title
        else:
            # Default case for other formats
            return title

get_markup(title, markup_format, level=1) classmethod

Generates a formatted header string based on the specified markup format.

Parameters:

Name Type Description Default
title str

The title to be formatted as the header.

required
markup_format str

The markup format for the header.

required
level int

The section level to generate a header for.

1

Returns:

Name Type Description
str str

The formatted header string.

Source code in ngwidgets/markup_header.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@classmethod
def get_markup(cls, title: str, markup_format: str, level: int = 1) -> str:
    """
    Generates a formatted header string based on the specified markup format.

    Args:
        title (str): The title to be formatted as the header.
        markup_format (str): The markup format for the header.
        level (int): The section level to generate a header for.

    Returns:
        str: The formatted header string.
    """
    if markup_format == "github":
        return f"{'#' * level} {title}\n"
    elif markup_format == "mediawiki":
        return f"{'=' * level} {title} {'=' * level}\n"
    elif markup_format == "html" or markup_format == "unsafehtml":
        return f"<h{level}>{title}</h{level}>"
    elif markup_format == "latex":
        if level == 1:
            return f"\\section{{{title}}}"
        elif level == 2:
            return f"\\subsection{{{title}}}"
        elif level == 3:
            return f"\\subsubsection{{{title}}}"
    elif markup_format == "textile":
        return f"h{level}. {title}"
    elif markup_format == "plain":
        return title
    else:
        # Default case for other formats
        return title

ngwidgets_cmd

Created on 2023-09-10

@author: wf

NiceguiWidgetsCmd

Bases: WebserverCmd

command line handling for ngwidgets

Source code in ngwidgets/ngwidgets_cmd.py
13
14
15
16
class NiceguiWidgetsCmd(WebserverCmd):
    """
    command line handling for ngwidgets
    """

main(argv=None)

main call

Source code in ngwidgets/ngwidgets_cmd.py
19
20
21
22
23
24
25
26
27
28
def main(argv: list = None):
    """
    main call
    """
    cmd = NiceguiWidgetsCmd(
        config=NiceGuiWidgetsDemoWebserver.get_config(),
        webserver_cls=NiceGuiWidgetsDemoWebserver,
    )
    exit_code = cmd.cmd_main(argv)
    return exit_code

orjson_response

Created on 2023-11-19

@author: wf

ORJSONResponse

Bases: JSONResponse

A FastAPI response class that uses orjson for JSON serialization.

Source code in ngwidgets/orjson_response.py
13
14
15
16
17
18
19
20
21
22
23
24
class ORJSONResponse(JSONResponse):
    """
    A FastAPI response class that uses orjson for JSON serialization.
    """

    media_type = "application/json"

    def render(self, content: Any) -> bytes:
        """
        Override the render method to use orjson for serialization.
        """
        return orjson.dumps(content)

render(content)

Override the render method to use orjson for serialization.

Source code in ngwidgets/orjson_response.py
20
21
22
23
24
def render(self, content: Any) -> bytes:
    """
    Override the render method to use orjson for serialization.
    """
    return orjson.dumps(content)

pdfviewer

Created on 2023-09-17

@author: wf

pdfjs_urls dataclass

see https://mozilla.github.io/pdf.js/getting_started/#download

setup the content delivery network urls

Source code in ngwidgets/pdfviewer.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@dataclass
class pdfjs_urls:
    """
    see https://mozilla.github.io/pdf.js/getting_started/#download

    setup the content delivery network urls
    """

    cdn: str = "jsdelivr"
    version: str = "3.10.111"
    debug: bool = False
    url = {}

    def configure(self):
        """ """
        version = self.version
        dot_min = "" if self.debug else ".min"
        # where to find library, css and
        l = "build/"
        c = "web/"
        v = l
        js = "pdf"
        if self.cdn == "github":
            self.base_url = "https://raw.githubusercontent.com/mozilla/pdf.js/master"
            l = c
            v = c
            # no minimized version available
            dot_min = ""
            js = "pdfjs"
        elif self.cdn == "cdnjs":
            self.base_url = f"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/{version}"
            c = ""
            v = ""
            l = ""
        elif self.cdn == "jsdelivr":
            self.base_url = f"https://cdn.jsdelivr.net/npm/pdfjs-dist@{version}"
            v = c
        elif self.cdn == "unpkg":
            # no minimized version available
            dot_min = ""
            v = c
            self.base_url = f"https://unpkg.com/pdfjs-dist@{version}"
        else:
            raise ValueError(f"unknown cdn {self.cdn}")
        css = f"pdf_viewer{dot_min}.css"
        lib = f"{js}{dot_min}.js"
        viewer = f"pdf_viewer{dot_min}.js"
        self.url["css"] = f"{self.base_url}/{c}{css}"
        self.url["js_lib"] = f"{self.base_url}/{l}{lib}"
        self.url["js_viewer"] = f"{self.base_url}/{v}{viewer}"

configure()

Source code in ngwidgets/pdfviewer.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def configure(self):
    """ """
    version = self.version
    dot_min = "" if self.debug else ".min"
    # where to find library, css and
    l = "build/"
    c = "web/"
    v = l
    js = "pdf"
    if self.cdn == "github":
        self.base_url = "https://raw.githubusercontent.com/mozilla/pdf.js/master"
        l = c
        v = c
        # no minimized version available
        dot_min = ""
        js = "pdfjs"
    elif self.cdn == "cdnjs":
        self.base_url = f"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/{version}"
        c = ""
        v = ""
        l = ""
    elif self.cdn == "jsdelivr":
        self.base_url = f"https://cdn.jsdelivr.net/npm/pdfjs-dist@{version}"
        v = c
    elif self.cdn == "unpkg":
        # no minimized version available
        dot_min = ""
        v = c
        self.base_url = f"https://unpkg.com/pdfjs-dist@{version}"
    else:
        raise ValueError(f"unknown cdn {self.cdn}")
    css = f"pdf_viewer{dot_min}.css"
    lib = f"{js}{dot_min}.js"
    viewer = f"pdf_viewer{dot_min}.js"
    self.url["css"] = f"{self.base_url}/{c}{css}"
    self.url["js_lib"] = f"{self.base_url}/{l}{lib}"
    self.url["js_viewer"] = f"{self.base_url}/{v}{viewer}"

pdfviewer

Bases: element

nicegui PDF.js integration

see https://mozilla.github.io/pdf.js/

Source code in ngwidgets/pdfviewer.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
class pdfviewer(ui.element, component="pdfviewer.js"):
    """
    nicegui PDF.js integration

    see https://mozilla.github.io/pdf.js/
    """

    def __init__(
        self, version: str = "3.11.174", cdn="cdnjs", debug: bool = False
    ) -> None:
        """
        constructor
        """
        super().__init__()

        self.version = version
        self.debug = debug
        self.urls = pdfjs_urls(cdn, version, debug)
        self.urls.configure()

        ui.add_head_html(f"""<link href="{self.urls.url['css']}" rel="stylesheet"/>""")
        ui.add_head_html(f"""<script src="{self.urls.url['js_lib']}"></script>""")
        ui.add_head_html(f"""<script src="{self.urls.url['js_viewer']}"></script>""")
        viewer_container_style = """<style>
  .pdfViewerContainer {
    overflow: auto;
    position: absolute;
    width: 100%;
    height: 100%;
  }
</style>"""
        ui.add_head_html(viewer_container_style)

    def load_pdf(self, pdf_url: str) -> None:
        self.run_method("load_pdf", pdf_url)

    def set_page(self, page_number: int) -> None:
        self.run_method("set_page", page_number)

__init__(version='3.11.174', cdn='cdnjs', debug=False)

constructor

Source code in ngwidgets/pdfviewer.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
    def __init__(
        self, version: str = "3.11.174", cdn="cdnjs", debug: bool = False
    ) -> None:
        """
        constructor
        """
        super().__init__()

        self.version = version
        self.debug = debug
        self.urls = pdfjs_urls(cdn, version, debug)
        self.urls.configure()

        ui.add_head_html(f"""<link href="{self.urls.url['css']}" rel="stylesheet"/>""")
        ui.add_head_html(f"""<script src="{self.urls.url['js_lib']}"></script>""")
        ui.add_head_html(f"""<script src="{self.urls.url['js_viewer']}"></script>""")
        viewer_container_style = """<style>
  .pdfViewerContainer {
    overflow: auto;
    position: absolute;
    width: 100%;
    height: 100%;
  }
</style>"""
        ui.add_head_html(viewer_container_style)

profiler

Created on 2022-11-18

@author: wf

Profiler

simple profiler

Source code in ngwidgets/profiler.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Profiler:
    """
    simple profiler
    """

    def __init__(self, msg, profile=True, with_start: bool = True):
        """
        construct me with the given msg and profile active flag

        Args:
            msg(str): the message to show if profiling is active
            profile(bool): True if messages should be shown
        """
        self.msg = msg
        self.profile = profile
        if with_start:
            self.start()

    def start(self):
        """
        start profiling
        """
        self.starttime = time.time()
        if self.profile:
            print(f"Starting {self.msg} ...")

    def time(self, extraMsg=""):
        """
        time the action and print if profile is active
        """
        elapsed = time.time() - self.starttime
        if self.profile:
            print(f"{self.msg}{extraMsg} took {elapsed:5.1f} s")
        return elapsed

__init__(msg, profile=True, with_start=True)

construct me with the given msg and profile active flag

Parameters:

Name Type Description Default
msg str

the message to show if profiling is active

required
profile bool

True if messages should be shown

True
Source code in ngwidgets/profiler.py
15
16
17
18
19
20
21
22
23
24
25
26
def __init__(self, msg, profile=True, with_start: bool = True):
    """
    construct me with the given msg and profile active flag

    Args:
        msg(str): the message to show if profiling is active
        profile(bool): True if messages should be shown
    """
    self.msg = msg
    self.profile = profile
    if with_start:
        self.start()

start()

start profiling

Source code in ngwidgets/profiler.py
28
29
30
31
32
33
34
def start(self):
    """
    start profiling
    """
    self.starttime = time.time()
    if self.profile:
        print(f"Starting {self.msg} ...")

time(extraMsg='')

time the action and print if profile is active

Source code in ngwidgets/profiler.py
36
37
38
39
40
41
42
43
def time(self, extraMsg=""):
    """
    time the action and print if profile is active
    """
    elapsed = time.time() - self.starttime
    if self.profile:
        print(f"{self.msg}{extraMsg} took {elapsed:5.1f} s")
    return elapsed

progress

NiceguiProgressbar

Bases: Progressbar

Nicegui progress bar wrapper.

Source code in ngwidgets/progress.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class NiceguiProgressbar(Progressbar):
    """
    Nicegui progress bar wrapper.
    """

    def __init__(self, total, desc: str, unit: str, label_color="#8D92C4"):
        """
        Initialize the NiceguiProgressbar instance.

        Args:
            total (int): The total value (maximum) of the progress bar.
            desc (str): A short description of the task for which the progress is being shown.
            unit (str): The unit of measurement for the progress (e.g., 'step', 'item').
            label_color(str): the color to use for the label
        The progress bar is initially set to invisible and its value to 0.
        """
        super().__init__(total, 0, desc, unit)
        self.progress = ui.linear_progress(
            value=0, size="20px", show_value=False
        ).props("instant-feedback")
        # Set the label color based on the provided color schema
        self.label_style = f"color: {label_color};"

        with self.progress:
            self.label = (
                ui.label().classes("text-lg absolute-center").style(self.label_style)
            )
            self.label.bind_text_from(
                self, "value", backward=lambda v: f"{self.desc} {v}/{self.total}"
            )
        self.progress.visible = False

    def reset(self):
        """
        reset
        """
        self.value = 0
        self.progress.value = 0

    def set_description(self, desc: str):
        """
        set my description
        """
        self.desc = desc
        self.progress.visible = True

    def update_value(self, new_value):
        self.value = new_value
        self.progress.visible = True
        percent = round(self.value / self.total, 2)
        self.progress.value = percent

    def update(self, step):
        self.update_value(self.value + step)

__init__(total, desc, unit, label_color='#8D92C4')

Initialize the NiceguiProgressbar instance.

Parameters:

Name Type Description Default
total int

The total value (maximum) of the progress bar.

required
desc str

A short description of the task for which the progress is being shown.

required
unit str

The unit of measurement for the progress (e.g., 'step', 'item').

required
label_color str

the color to use for the label

'#8D92C4'

The progress bar is initially set to invisible and its value to 0.

Source code in ngwidgets/progress.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(self, total, desc: str, unit: str, label_color="#8D92C4"):
    """
    Initialize the NiceguiProgressbar instance.

    Args:
        total (int): The total value (maximum) of the progress bar.
        desc (str): A short description of the task for which the progress is being shown.
        unit (str): The unit of measurement for the progress (e.g., 'step', 'item').
        label_color(str): the color to use for the label
    The progress bar is initially set to invisible and its value to 0.
    """
    super().__init__(total, 0, desc, unit)
    self.progress = ui.linear_progress(
        value=0, size="20px", show_value=False
    ).props("instant-feedback")
    # Set the label color based on the provided color schema
    self.label_style = f"color: {label_color};"

    with self.progress:
        self.label = (
            ui.label().classes("text-lg absolute-center").style(self.label_style)
        )
        self.label.bind_text_from(
            self, "value", backward=lambda v: f"{self.desc} {v}/{self.total}"
        )
    self.progress.visible = False

reset()

reset

Source code in ngwidgets/progress.py
66
67
68
69
70
71
def reset(self):
    """
    reset
    """
    self.value = 0
    self.progress.value = 0

set_description(desc)

set my description

Source code in ngwidgets/progress.py
73
74
75
76
77
78
def set_description(self, desc: str):
    """
    set my description
    """
    self.desc = desc
    self.progress.visible = True

Progressbar dataclass

Generic progress bar

Source code in ngwidgets/progress.py
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@dataclass
class Progressbar:
    """
    Generic progress bar
    """

    _total: int
    value: int
    desc: str
    unit: str

    @property
    def total(self) -> int:
        return self._total

    @total.setter
    def total(self, total: int):
        self._total = total
        self.update_total()

    def update_total(self):
        """
        Update the total value in the progress bar.
        """
        pass

update_total()

Update the total value in the progress bar.

Source code in ngwidgets/progress.py
27
28
29
30
31
def update_total(self):
    """
    Update the total value in the progress bar.
    """
    pass

TqdmProgressbar

Bases: Progressbar

Tqdm progress bar wrapper.

Source code in ngwidgets/progress.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
class TqdmProgressbar(Progressbar):
    """
    Tqdm progress bar wrapper.
    """

    def __init__(self, total, desc, unit):
        super().__init__(total, 0, desc, unit)
        self.reset()

    def reset(self):
        self.progress = tqdm(total=self.total, desc=self.desc, unit=self.unit)
        self.value = 0

    def set_description(self, desc: str):
        self.progress.set_description(desc)

    def update(self, step):
        self.update_value(self.value + step)

    def update_value(self, new_value):
        increment = new_value - self.value
        self.value = new_value
        self.progress.update(increment)

    def update_total(self):
        self.progress.total = self.total
        self.progress.refresh()

projects

Created on 2023-12-14

This module, developed as part of the ngwidgets package under the instruction of WF, provides classes and methods for interacting with the Python Package Index (PyPI). It includes the Project data class for representing software projects and the PyPi class for searching and retrieving package information from PyPI. The code facilitates the creation of tools and applications that interact with PyPI for information about Python packages.

Prompts for LLM: - Create Python classes Project and Projects (holding a list of Project elements) for interacting with PyPI and github, including search functionality. - Develop a data class in Python to represent a software project with the attributes. name (str): The name of the project. package (str): The package name on PyPI. demo (str): A URL to a demo of the project, if available. forum_post (str): A URL to a forum post discussing the project. github (str): A URL to the GitHub repository of the project. pypi (str): A URL to the PyPI page of the project. image_url (str): A URL to an image representing the project. stars (int): Number of stars on GitHub. github_description (str): Description of the project from GitHub. pypi_description (str): Description of the project from PyPI. avatar (str): A URL to the avatar image of the author/maintainer. search_text (str): Text used for searching the project. github_author (str): The GitHub username of the project's author. pypi_author (str): The PyPI username of the project's author. created_at (datetime): The date when the project was created. downloads (int): Number of downloads from PyPI. categories (List[str]): Categories associated with the project. version (str): The current version of the project on PyPI.

  • Implement methods to search PyPI and github for packages/repos that represent projects and retrieve detailed package information on a given topic.
  • allow saving and loading the collected projects

Main author: OpenAI's language model (instructed by WF)

GitHubAccess

A class to handle GitHub API access.

This class provides functionalities to access the GitHub API, either with authenticated or unauthenticated access. It can read a GitHub access token from a YAML file in a specified directory for authenticated access, which increases the rate limit for API requests. If no access token is provided or found in the YAML file, it defaults to unauthenticated access with lower rate limits.

Attributes:

Name Type Description
github Github

An instance of the Github class from the PyGithub library, configured for either authenticated or unauthenticated access.

Source code in ngwidgets/projects.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class GitHubAccess:
    """
    A class to handle GitHub API access.

    This class provides functionalities to access the GitHub API, either with authenticated or unauthenticated access.
    It can read a GitHub access token from a YAML file in a specified directory for authenticated access,
    which increases the rate limit for API requests. If no access token is provided or found in the YAML file,
    it defaults to unauthenticated access with lower rate limits.

    Attributes:
        github (Github): An instance of the Github class from the PyGithub library, configured for either authenticated or unauthenticated access.
    """

    def __init__(
        self,
        default_directory: str = None,
        access_token: Optional[str] = None,
        per_page: int = 100,
    ):
        """
        Initialize the GitHub instance.

        If an access_token is provided, use it for authenticated access to increase the rate limit.
        Otherwise, attempt to read the access token from a YAML file in the default directory.
        If no token is found, access is unauthenticated with lower rate limits.

        Args:
            default_directory (str): Path to the directory where the access token file is stored.
            access_token (Optional[str]): A GitHub personal access token. Defaults to None.
            per_page (int): Page size for API results. The search API returns at most 1000 results
                and allows 30 requests per minute, so the PyGithub default of 30 needs 34 requests
                and runs into 403 Forbidden with backoff; 100 - the GitHub maximum - needs 10.
        """
        if not access_token and default_directory:
            access_token = self._read_access_token(default_directory)
        self.github = Github(access_token, per_page=per_page)

    def _read_access_token(self, default_directory: str) -> Optional[str]:
        """
        Read the GitHub access token from a YAML file located in the default directory.

        Args:
            default_directory (str): Path to the directory where the access token file is stored.

        Returns:
            Optional[str]: The access token if found, otherwise None.
        """
        token_file = Path(default_directory) / "github_access_token.yaml"
        if token_file.exists():
            with open(token_file, "r") as file:
                data = yaml.safe_load(file)
                return data.get("access_token", None)
        return None

    def search_repositories(self, query: str) -> dict:
        """
        Search for GitHub repositories matching a given query.

        Args:
            query (str): The search query string.

        Returns:
            dict: A dictionary of repository objects keyed by their full names.
        """
        repositories = self.github.search_repositories(query)
        repo_dict = {repo.full_name: repo for repo in repositories}
        return repo_dict

__init__(default_directory=None, access_token=None, per_page=100)

Initialize the GitHub instance.

If an access_token is provided, use it for authenticated access to increase the rate limit. Otherwise, attempt to read the access token from a YAML file in the default directory. If no token is found, access is unauthenticated with lower rate limits.

Parameters:

Name Type Description Default
default_directory str

Path to the directory where the access token file is stored.

None
access_token Optional[str]

A GitHub personal access token. Defaults to None.

None
per_page int

Page size for API results. The search API returns at most 1000 results and allows 30 requests per minute, so the PyGithub default of 30 needs 34 requests and runs into 403 Forbidden with backoff; 100 - the GitHub maximum - needs 10.

100
Source code in ngwidgets/projects.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def __init__(
    self,
    default_directory: str = None,
    access_token: Optional[str] = None,
    per_page: int = 100,
):
    """
    Initialize the GitHub instance.

    If an access_token is provided, use it for authenticated access to increase the rate limit.
    Otherwise, attempt to read the access token from a YAML file in the default directory.
    If no token is found, access is unauthenticated with lower rate limits.

    Args:
        default_directory (str): Path to the directory where the access token file is stored.
        access_token (Optional[str]): A GitHub personal access token. Defaults to None.
        per_page (int): Page size for API results. The search API returns at most 1000 results
            and allows 30 requests per minute, so the PyGithub default of 30 needs 34 requests
            and runs into 403 Forbidden with backoff; 100 - the GitHub maximum - needs 10.
    """
    if not access_token and default_directory:
        access_token = self._read_access_token(default_directory)
    self.github = Github(access_token, per_page=per_page)

search_repositories(query)

Search for GitHub repositories matching a given query.

Parameters:

Name Type Description Default
query str

The search query string.

required

Returns:

Name Type Description
dict dict

A dictionary of repository objects keyed by their full names.

Source code in ngwidgets/projects.py
112
113
114
115
116
117
118
119
120
121
122
123
124
def search_repositories(self, query: str) -> dict:
    """
    Search for GitHub repositories matching a given query.

    Args:
        query (str): The search query string.

    Returns:
        dict: A dictionary of repository objects keyed by their full names.
    """
    repositories = self.github.search_repositories(query)
    repo_dict = {repo.full_name: repo for repo in repositories}
    return repo_dict

Project

A data class representing a software project, potentially from PyPI or GitHub.

Attributes:

Name Type Description
name str

The name of the project.

package str

The package name on PyPI.

demo str

A URL to a demo of the project, if available.

forum_post str

A URL to a forum post discussing the project.

github str

A URL to the GitHub repository of the project.

pypi str

A URL to the PyPI page of the project.

image_url str

A URL to an image representing the project.

stars int

Number of stars on GitHub.

github_description str

Description of the project from GitHub.

pypi_description str

Description of the project from PyPI.

avatar str

A URL to the avatar image of the author/maintainer.

search_text str

Text used for searching the project.

github_author str

The GitHub username of the project's author.

pypi_author str

The PyPI username of the project's author.

created_at datetime

The date when the project was created.

downloads int

Number of downloads from PyPI.

categories List[str]

Categories associated with the project.

version str

The current version of the project on PyPI.

Solution bazaar attributes

component_url(str): the url of a yaml file with component declarations, demo, install and usage information solution_tags(str): a list of comma separated tags for checking the conformance of the project to the solution bazaar guidelines

Source code in ngwidgets/projects.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
@lod_storable
class Project:
    """
    A data class representing a software project, potentially from PyPI or GitHub.

    Attributes:
        name (str): The name of the project.
        package (str): The package name on PyPI.
        demo (str): A URL to a demo of the project, if available.
        forum_post (str): A URL to a forum post discussing the project.
        github (str): A URL to the GitHub repository of the project.
        pypi (str): A URL to the PyPI page of the project.
        image_url (str): A URL to an image representing the project.
        stars (int): Number of stars on GitHub.
        github_description (str): Description of the project from GitHub.
        pypi_description (str): Description of the project from PyPI.
        avatar (str): A URL to the avatar image of the author/maintainer.
        search_text (str): Text used for searching the project.
        github_author (str): The GitHub username of the project's author.
        pypi_author (str): The PyPI username of the project's author.
        created_at (datetime): The date when the project was created.
        downloads (int): Number of downloads from PyPI.
        categories (List[str]): Categories associated with the project.
        version (str): The current version of the project on PyPI.

    Solution bazaar attributes:
        component_url(str): the url of a yaml file with component declarations, demo, install and usage information
        solution_tags(str): a list of comma separated tags for checking the conformance of the project
        to the solution bazaar guidelines
    """

    name: Optional[str] = None
    package: Optional[str] = None
    demo: Optional[str] = None
    forum_post: Optional[str] = None
    github_owner: Optional[str] = None
    github_repo_name: Optional[str] = None
    github: Optional[str] = None
    pypi: Optional[str] = None
    image_url: Optional[str] = None
    stars: Optional[int] = None
    github_description: Optional[str] = None
    pypi_description: Optional[str] = None
    avatar: Optional[str] = None
    search_text: Optional[str] = None
    github_author: Optional[str] = None
    pypi_author: Optional[str] = None
    created_at: Optional[datetime] = None
    downloads: Optional[int] = None
    categories: List[str] = field(default_factory=list)
    version: Optional[str] = None
    # solution bazaar properties
    components_url: Optional[str] = None
    solution_tags: Optional[str] = ""
    solution_id: Optional[str] = None

    def __post_init__(self):
        if self.github_owner and self.github_repo_name:
            self.solution_id = self._generate_solution_id()

    def _generate_solution_id(self) -> str:
        owner = self.github_owner or ""
        repo_name = self.github_repo_name or ""
        base_id = f"{owner}_{repo_name}"
        base_id = base_id.replace("/", "_").replace(
            "\\", "_"
        )  # Replace slashes with underscores
        normalized_id = (
            unicodedata.normalize("NFKD", base_id)
            .encode("ascii", "ignore")
            .decode("ascii")
        )
        return re.sub(r"[^\w\s.-]", "", normalized_id)

    @property
    def component_count(self) -> int:
        """
        Counts the number of components associated with the project.
        Returns 0 if there are no components or if components_url is not set.
        """
        if not self.components_url:
            return 0
        components = self.get_components()
        return len(components.components) if components else 0

    @property
    def install_instructions(self) -> str:
        """
        Get the installation instructions for the project.

        Returns:
            str: Installation instructions for the project.
        """
        return f"pip install {self.package}"

    def get_components(
        self, cache_directory: str = None, cache_valid_secs: int = 3600
    ) -> Components:
        """
        method to lazy-loaded components. Loads components from URL if components_url is set.
        If a cache directory is provided, it caches the YAML file in that directory. The cache validity period
        can be specified in seconds.

        Args:
            cache_directory (str, optional): Directory for caching the YAML files. If None, caching is disabled.
            cache_valid_secs (int, optional): The number of seconds for which the cache is considered valid. Defaults to 3600 seconds (1 hour).

        Returns:
            Components: The components associated with the project.
        """
        if not self.components_url:
            return None

        # (slow) load from url is the default
        load_from_url = True

        # potentially we speed up by caching
        if cache_directory:
            cache_directory = Path(cache_directory) / "components"
            os.makedirs(cache_directory, exist_ok=True)
            filename = f"{self.solution_id}.yaml"
            file_path = cache_directory / filename

            if file_path.exists():
                file_size = file_path.stat().st_size
                if file_size > 0 and not self._is_file_outdated(
                    file_path, cache_valid_secs
                ):
                    load_from_url = False
                    components = Components.load_from_yaml_file(
                        str(file_path)
                    )  # @UndefinedVariable

        if load_from_url:
            components = Components.load_from_yaml_url(
                self.components_url
            )  # @UndefinedVariable
            if cache_directory:
                components.save_to_yaml_file(str(file_path))

        return components

    def _is_file_outdated(self, file_path: Path, cache_valid_secs: int = 3600) -> bool:
        """
        Check if the file is outdated (older than 1 hour).
        """
        file_mod_time = file_path.stat().st_mtime
        return (time.time() - file_mod_time) > cache_valid_secs

    def merge_pypi(self, pypi):
        """
        merge the given pypi project info to with mine
        """
        self.pypi = pypi.pypi
        self.package = pypi.package
        self.pypi_description = pypi.pypi_description
        self.version = pypi.version

    @classmethod
    def get_raw_url(
        cls, owner: str, repo_name: str, branch_name: str, file_path: str
    ) -> str:
        """
        Construct the URL for the raw  file_path from the owner, repository name, and branch name.

        Args:
            owner (str): The owner of the GitHub repository.
            repo_name (str): The name of the GitHub repository.
            branch_name (str): The name of the branch.
            file_path(str): the file_path to get the raw content for

        Returns:
            str: The URL of the raw file_path if it exists

        """
        raw_url = f"https://raw.githubusercontent.com/{owner}/{repo_name}/{branch_name}{file_path}"
        try:
            # Attempt to open the raw URL
            with urllib.request.urlopen(raw_url) as response:
                # Check if the response status code is 200 (OK)
                if response.getcode() == 200:
                    return raw_url
        except urllib.error.URLError as ex:

            pass  # Handle any exceptions here
        return None  # Return None if .component.yaml doesn't exist

    @classmethod
    def from_github(cls, repo) -> "Project":
        """
        Class method to create a Project instance from a GitHub repository.

        Args:
            repo(Repository.Repository): The github repository
            github_access (GitHubAccess): Instance of GitHubAccess for API calls.

        Returns:
            Project: An instance of the Project class filled with GitHub repository details.
        """
        avatar_url = repo.owner.avatar_url if repo.owner.avatar_url else None
        stars = repo.stargazers_count
        owner = repo.owner.login
        repo_name = repo.name

        components_url = cls.get_raw_url(
            owner, repo_name, repo.default_branch, "/.components.yaml"
        )
        project = cls(
            name=repo.name,
            github=repo.html_url,
            github_repo_name=repo.name,
            github_owner=repo.owner.login,
            stars=stars,
            github_description=repo.description,
            github_author=repo.owner.login,
            created_at=repo.created_at,
            avatar=avatar_url,
            components_url=components_url,
            # Other fields can be filled in as needed
        )
        return project

    @classmethod
    def from_pypi(cls, package_info: Dict) -> "Project":
        """
        Class method to create a Project instance from a PyPI package.

        Args:
            package_info (Dict): Dictionary containing package data from PyPI.

        Returns:
            Project: An instance of the Project class filled with PyPI package details.
        """
        info = package_info.get("info", {})
        github = None
        project_urls = info.get("project_urls", {})
        if project_urls:
            # Preferred keys for GitHub URLs
            preferred_keys = ["Repository", "Source", "Home"]
            github_base_url = "https://github.com/"

            # Iterate over the preferred keys and check if any URL starts with the GitHub base URL
            for key in preferred_keys:
                url = project_urls.get(key)
                if url and url.startswith(github_base_url):
                    github = url
                    break
            else:
                # If no GitHub URL is found, you may choose to handle this case (e.g., logging, fallback logic)
                github = None

        project = cls(
            name=info.get("name"),
            package=info.get("name"),
            pypi=info.get("package_url"),
            pypi_description=info.get("summary"),
            version=info.get("version"),
            github_description=info.get("description"),
            github=github,
        )
        return project

component_count property

Counts the number of components associated with the project. Returns 0 if there are no components or if components_url is not set.

install_instructions property

Get the installation instructions for the project.

Returns:

Name Type Description
str str

Installation instructions for the project.

from_github(repo) classmethod

Class method to create a Project instance from a GitHub repository.

Parameters:

Name Type Description Default
repo Repository

The github repository

required
github_access GitHubAccess

Instance of GitHubAccess for API calls.

required

Returns:

Name Type Description
Project Project

An instance of the Project class filled with GitHub repository details.

Source code in ngwidgets/projects.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
@classmethod
def from_github(cls, repo) -> "Project":
    """
    Class method to create a Project instance from a GitHub repository.

    Args:
        repo(Repository.Repository): The github repository
        github_access (GitHubAccess): Instance of GitHubAccess for API calls.

    Returns:
        Project: An instance of the Project class filled with GitHub repository details.
    """
    avatar_url = repo.owner.avatar_url if repo.owner.avatar_url else None
    stars = repo.stargazers_count
    owner = repo.owner.login
    repo_name = repo.name

    components_url = cls.get_raw_url(
        owner, repo_name, repo.default_branch, "/.components.yaml"
    )
    project = cls(
        name=repo.name,
        github=repo.html_url,
        github_repo_name=repo.name,
        github_owner=repo.owner.login,
        stars=stars,
        github_description=repo.description,
        github_author=repo.owner.login,
        created_at=repo.created_at,
        avatar=avatar_url,
        components_url=components_url,
        # Other fields can be filled in as needed
    )
    return project

from_pypi(package_info) classmethod

Class method to create a Project instance from a PyPI package.

Parameters:

Name Type Description Default
package_info Dict

Dictionary containing package data from PyPI.

required

Returns:

Name Type Description
Project Project

An instance of the Project class filled with PyPI package details.

Source code in ngwidgets/projects.py
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
@classmethod
def from_pypi(cls, package_info: Dict) -> "Project":
    """
    Class method to create a Project instance from a PyPI package.

    Args:
        package_info (Dict): Dictionary containing package data from PyPI.

    Returns:
        Project: An instance of the Project class filled with PyPI package details.
    """
    info = package_info.get("info", {})
    github = None
    project_urls = info.get("project_urls", {})
    if project_urls:
        # Preferred keys for GitHub URLs
        preferred_keys = ["Repository", "Source", "Home"]
        github_base_url = "https://github.com/"

        # Iterate over the preferred keys and check if any URL starts with the GitHub base URL
        for key in preferred_keys:
            url = project_urls.get(key)
            if url and url.startswith(github_base_url):
                github = url
                break
        else:
            # If no GitHub URL is found, you may choose to handle this case (e.g., logging, fallback logic)
            github = None

    project = cls(
        name=info.get("name"),
        package=info.get("name"),
        pypi=info.get("package_url"),
        pypi_description=info.get("summary"),
        version=info.get("version"),
        github_description=info.get("description"),
        github=github,
    )
    return project

get_components(cache_directory=None, cache_valid_secs=3600)

method to lazy-loaded components. Loads components from URL if components_url is set. If a cache directory is provided, it caches the YAML file in that directory. The cache validity period can be specified in seconds.

Parameters:

Name Type Description Default
cache_directory str

Directory for caching the YAML files. If None, caching is disabled.

None
cache_valid_secs int

The number of seconds for which the cache is considered valid. Defaults to 3600 seconds (1 hour).

3600

Returns:

Name Type Description
Components Components

The components associated with the project.

Source code in ngwidgets/projects.py
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
def get_components(
    self, cache_directory: str = None, cache_valid_secs: int = 3600
) -> Components:
    """
    method to lazy-loaded components. Loads components from URL if components_url is set.
    If a cache directory is provided, it caches the YAML file in that directory. The cache validity period
    can be specified in seconds.

    Args:
        cache_directory (str, optional): Directory for caching the YAML files. If None, caching is disabled.
        cache_valid_secs (int, optional): The number of seconds for which the cache is considered valid. Defaults to 3600 seconds (1 hour).

    Returns:
        Components: The components associated with the project.
    """
    if not self.components_url:
        return None

    # (slow) load from url is the default
    load_from_url = True

    # potentially we speed up by caching
    if cache_directory:
        cache_directory = Path(cache_directory) / "components"
        os.makedirs(cache_directory, exist_ok=True)
        filename = f"{self.solution_id}.yaml"
        file_path = cache_directory / filename

        if file_path.exists():
            file_size = file_path.stat().st_size
            if file_size > 0 and not self._is_file_outdated(
                file_path, cache_valid_secs
            ):
                load_from_url = False
                components = Components.load_from_yaml_file(
                    str(file_path)
                )  # @UndefinedVariable

    if load_from_url:
        components = Components.load_from_yaml_url(
            self.components_url
        )  # @UndefinedVariable
        if cache_directory:
            components.save_to_yaml_file(str(file_path))

    return components

get_raw_url(owner, repo_name, branch_name, file_path) classmethod

Construct the URL for the raw file_path from the owner, repository name, and branch name.

Parameters:

Name Type Description Default
owner str

The owner of the GitHub repository.

required
repo_name str

The name of the GitHub repository.

required
branch_name str

The name of the branch.

required
file_path str

the file_path to get the raw content for

required

Returns:

Name Type Description
str str

The URL of the raw file_path if it exists

Source code in ngwidgets/projects.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
@classmethod
def get_raw_url(
    cls, owner: str, repo_name: str, branch_name: str, file_path: str
) -> str:
    """
    Construct the URL for the raw  file_path from the owner, repository name, and branch name.

    Args:
        owner (str): The owner of the GitHub repository.
        repo_name (str): The name of the GitHub repository.
        branch_name (str): The name of the branch.
        file_path(str): the file_path to get the raw content for

    Returns:
        str: The URL of the raw file_path if it exists

    """
    raw_url = f"https://raw.githubusercontent.com/{owner}/{repo_name}/{branch_name}{file_path}"
    try:
        # Attempt to open the raw URL
        with urllib.request.urlopen(raw_url) as response:
            # Check if the response status code is 200 (OK)
            if response.getcode() == 200:
                return raw_url
    except urllib.error.URLError as ex:

        pass  # Handle any exceptions here
    return None  # Return None if .component.yaml doesn't exist

merge_pypi(pypi)

merge the given pypi project info to with mine

Source code in ngwidgets/projects.py
276
277
278
279
280
281
282
283
def merge_pypi(self, pypi):
    """
    merge the given pypi project info to with mine
    """
    self.pypi = pypi.pypi
    self.package = pypi.package
    self.pypi_description = pypi.pypi_description
    self.version = pypi.version

Projects

handle a list of python projects on a specific topic

Source code in ngwidgets/projects.py
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
@lod_storable
class Projects:
    """
    handle a list of python projects on a specific topic
    """

    topic: str
    _default_directory: Path = field(init=False)
    projects: List = field(default_factory=list, init=False)
    last_update_time: datetime = field(init=False)

    def __post_init__(self):
        """
        Post-initialization to set non-static attributes.
        """
        self._default_directory = Path.home() / ".nicegui"
        self.last_update_time = self.get_file_update_time()

    def get_file_update_time(self):
        """
        Get the last modification time of the projects file.

        Returns:
            datetime: The last modification time of the file or None if file does not exist.
        """
        if self.file_path.exists():
            file_mod_time = os.path.getmtime(self.file_path)
            return datetime.fromtimestamp(file_mod_time)
        return None

    @property
    def default_directory(self) -> Path:
        """
        The default directory for saving and loading projects.
        Returns:
            Path: The default directory path.
        """
        return self._default_directory

    @default_directory.setter
    def default_directory(self, directory: str):
        """
        Set the default directory for saving and loading projects.
        Args:
            directory (str): The path to the new default directory.
        """
        self._default_directory = Path(directory)

    @property
    def file_path(self) -> Path:
        """
        The file path for saving and loading projects, based on the topic.
        Returns:
            Path: The file path.
        """
        filename = f"components_{self.topic}.json"
        return self._default_directory / filename

    def get_project4_solution_id(self, solution_id: str) -> Project:
        """
        Get a project based on the provided solution_id.

        Args:
            solution_id (str): The solution_id to search for.

        Returns:
            Project: The Project instance matching the provided solution_id, or None if not found.
        """
        for project in self.projects:
            if project.solution_id == solution_id:
                return project
        return None

    def save(self, projects: List[Project] = None, directory: str = None):
        """
        Save a list of Project instances to a JSON file.
        Args:
            projects (List[Project]): A list of Project instances to be saved.
            directory (str, optional): The directory where the file will be saved. If None, uses the default directory.
        """
        if projects is None:
            projects = self.projects
        directory = Path(directory or self.default_directory)
        os.makedirs(directory, exist_ok=True)

        with open(self.file_path, "w", encoding="utf-8") as file:
            json.dump(
                [project.__dict__ for project in projects], file, indent=4, default=str
            )

    def load(
        self, directory: str = None, set_self: bool = True, lenient: bool = True
    ) -> List[Project]:
        """
        Load a list of Project instances from a JSON file.
        Args:
            directory (str, optional): The directory where the file is located. If None, uses the default directory.
            set_self(bool): if True set self.projects with the result
            lenient(bool): if True allow that there is no projects json file
        Returns:
            List[Project]: A list of Project instances loaded from the file.

        """
        directory = Path(directory or self.default_directory)
        projects = []
        if not self.file_path.exists():
            if not lenient:
                raise FileNotFoundError(f"No such file: {self.file_path}")
        else:
            with open(self.file_path, "r", encoding="utf-8") as file:
                projects_records = json.load(file)

            for project_record in projects_records:
                project = Project(**project_record)
                projects.append(project)
        if set_self:
            self.projects = projects
        return projects

    def get_github_projects(
        self, repo_dict: dict, progress_bar=None
    ) -> Dict[str, Project]:
        """
        Get GitHub projects related to the specified topic.

        Args:
            github_access (GitHubAccess): An instance of GitHubAccess for API calls.

        Returns:
            Dict[str, Project]: A dictionary of GitHub projects with their URLs as keys and Project instances as values.
        """
        projects_by_url = {}
        for repo in repo_dict.values():
            if progress_bar:
                progress_bar.update(1)
            project = Project.from_github(repo)
            projects_by_url[repo.html_url] = project
        return projects_by_url

    def sort_projects(self, projects: List[Project], sort_key: str):
        """
        Sorts a list of projects based on the specified sort key, converting integers to fixed-length strings.

        Args:
            projects (list): List of Project instances.
            sort_key (str): Attribute name to sort the projects by.

        Returns:
            list: Sorted list of projects.
        """

        # Define the function to determine the sorting value
        def get_sort_value(proj):
            attr = getattr(proj, sort_key, None)

            # Handle None values; place them at the end of the sorted list
            if attr is None:
                return " "  # Assuming you want None values to appear last

            # Convert integers to zero-padded strings, and others to strings
            if isinstance(attr, int):
                return f"{attr:010d}"  # Zero-pad to 10 digits
            else:
                return str(attr).lower()

        # Determine if sorting should be in reverse
        reverse_sort = sort_key in ["stars", "downloads", "component_count"]

        return sorted(projects, key=get_sort_value, reverse=reverse_sort)

    def update(
        self,
        progress_bar: Optional[Progressbar] = None,
        limit_github: Optional[int] = None,
        limit_pypi: Optional[int] = None,
    ):
        """
        Update the list of projects by retrieving potential projects from PyPI and GitHub based on the topic.

        Args:
            progress_bar (Optional[Progressbar]): A progress bar instance to update during the process.
            limit_github (Optional[int]): If set, limit the maximum number of GitHub projects to retrieve.
            limit_pypi (Optional[int]): If set, limit the maximum number of PyPI projects to retrieve.
        """
        # Initialize progress bar if provided
        if progress_bar:
            cached_projects = self.load()
            progress_bar.total = len(cached_projects)
            progress_bar.reset()
            progress_bar.set_description("Updating projects")

        # pypi access
        pypi = PyPi()

        # Fetch projects from PyPI
        pypi_projects = pypi.search_projects(self.topic)
        # Apply limit to the PyPI projects
        if limit_pypi is not None:
            pypi_projects = pypi_projects[:limit_pypi]
        # Fetch repositories from GitHub
        github_access = GitHubAccess(self.default_directory)
        query = self.topic
        repo_dict = github_access.search_repositories(query)
        # Apply limit to the GitHub repositories
        if limit_github is not None:
            repo_dict = dict(list(repo_dict.items())[:limit_github])
        total = len(repo_dict) + len(pypi_projects)
        if progress_bar:
            progress_bar.total = total

        projects_by_github_url = self.get_github_projects(repo_dict, progress_bar)
        self.projects = list(projects_by_github_url.values())

        # Merge PyPI projects into the GitHub projects
        for pypi in pypi_projects:
            matched_project = None  # Reset for each PyPI project
            if pypi.github:
                for github_url in projects_by_github_url.keys():
                    if pypi.github.startswith(github_url):
                        matched_project = projects_by_github_url[github_url]
                if matched_project:
                    matched_project.merge_pypi(pypi)
                else:
                    # we have github url but it was not in our search list
                    # check the gitub repo for more details
                    repo_name = self.extract_repo_name_from_url(pypi.github)
                    if not repo_name:
                        raise ValueError(
                            f"Can't determine repo_name for {pypi.github} of pypi package {pypi.package}"
                        )
                    # Create a Project instance from GitHub
                    repo = github_access.github.get_repo(repo_name)
                    github_comp = Project.from_github(repo)
                    # Merge PyPI data into the newly created GitHub project
                    github_comp.merge_pypi(pypi)
                    self.projects.append(github_comp)
            else:
                # PyPI project without a GitHub URL
                self.projects.append(pypi)
            if progress_bar:
                progress_bar.update(1)
        # sort projects by name
        self.projects = sorted(
            self.projects, key=lambda comp: comp.name.lower() if comp.name else ""
        )
        self.last_update_time = datetime.now()

    def extract_repo_name_from_url(self, url: str) -> str:
        """
        Extract the repository name in 'user/repo' format from a GitHub URL.

        Args:
            url (str): The GitHub URL.

        Returns:
            str: The repository name or None if not extractable.
        """
        # Assuming the URL format is https://github.com/user/repo
        parts = url.split("/")
        if len(parts) > 4 and parts[2] == "github.com":
            return "/".join(parts[3:5])
        return None

default_directory property writable

The default directory for saving and loading projects. Returns: Path: The default directory path.

file_path property

The file path for saving and loading projects, based on the topic. Returns: Path: The file path.

__post_init__()

Post-initialization to set non-static attributes.

Source code in ngwidgets/projects.py
401
402
403
404
405
406
def __post_init__(self):
    """
    Post-initialization to set non-static attributes.
    """
    self._default_directory = Path.home() / ".nicegui"
    self.last_update_time = self.get_file_update_time()

extract_repo_name_from_url(url)

Extract the repository name in 'user/repo' format from a GitHub URL.

Parameters:

Name Type Description Default
url str

The GitHub URL.

required

Returns:

Name Type Description
str str

The repository name or None if not extractable.

Source code in ngwidgets/projects.py
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
def extract_repo_name_from_url(self, url: str) -> str:
    """
    Extract the repository name in 'user/repo' format from a GitHub URL.

    Args:
        url (str): The GitHub URL.

    Returns:
        str: The repository name or None if not extractable.
    """
    # Assuming the URL format is https://github.com/user/repo
    parts = url.split("/")
    if len(parts) > 4 and parts[2] == "github.com":
        return "/".join(parts[3:5])
    return None

get_file_update_time()

Get the last modification time of the projects file.

Returns:

Name Type Description
datetime

The last modification time of the file or None if file does not exist.

Source code in ngwidgets/projects.py
408
409
410
411
412
413
414
415
416
417
418
def get_file_update_time(self):
    """
    Get the last modification time of the projects file.

    Returns:
        datetime: The last modification time of the file or None if file does not exist.
    """
    if self.file_path.exists():
        file_mod_time = os.path.getmtime(self.file_path)
        return datetime.fromtimestamp(file_mod_time)
    return None

get_github_projects(repo_dict, progress_bar=None)

Get GitHub projects related to the specified topic.

Parameters:

Name Type Description Default
github_access GitHubAccess

An instance of GitHubAccess for API calls.

required

Returns:

Type Description
Dict[str, Project]

Dict[str, Project]: A dictionary of GitHub projects with their URLs as keys and Project instances as values.

Source code in ngwidgets/projects.py
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
def get_github_projects(
    self, repo_dict: dict, progress_bar=None
) -> Dict[str, Project]:
    """
    Get GitHub projects related to the specified topic.

    Args:
        github_access (GitHubAccess): An instance of GitHubAccess for API calls.

    Returns:
        Dict[str, Project]: A dictionary of GitHub projects with their URLs as keys and Project instances as values.
    """
    projects_by_url = {}
    for repo in repo_dict.values():
        if progress_bar:
            progress_bar.update(1)
        project = Project.from_github(repo)
        projects_by_url[repo.html_url] = project
    return projects_by_url

get_project4_solution_id(solution_id)

Get a project based on the provided solution_id.

Parameters:

Name Type Description Default
solution_id str

The solution_id to search for.

required

Returns:

Name Type Description
Project Project

The Project instance matching the provided solution_id, or None if not found.

Source code in ngwidgets/projects.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def get_project4_solution_id(self, solution_id: str) -> Project:
    """
    Get a project based on the provided solution_id.

    Args:
        solution_id (str): The solution_id to search for.

    Returns:
        Project: The Project instance matching the provided solution_id, or None if not found.
    """
    for project in self.projects:
        if project.solution_id == solution_id:
            return project
    return None

load(directory=None, set_self=True, lenient=True)

Load a list of Project instances from a JSON file. Args: directory (str, optional): The directory where the file is located. If None, uses the default directory. set_self(bool): if True set self.projects with the result lenient(bool): if True allow that there is no projects json file Returns: List[Project]: A list of Project instances loaded from the file.

Source code in ngwidgets/projects.py
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
def load(
    self, directory: str = None, set_self: bool = True, lenient: bool = True
) -> List[Project]:
    """
    Load a list of Project instances from a JSON file.
    Args:
        directory (str, optional): The directory where the file is located. If None, uses the default directory.
        set_self(bool): if True set self.projects with the result
        lenient(bool): if True allow that there is no projects json file
    Returns:
        List[Project]: A list of Project instances loaded from the file.

    """
    directory = Path(directory or self.default_directory)
    projects = []
    if not self.file_path.exists():
        if not lenient:
            raise FileNotFoundError(f"No such file: {self.file_path}")
    else:
        with open(self.file_path, "r", encoding="utf-8") as file:
            projects_records = json.load(file)

        for project_record in projects_records:
            project = Project(**project_record)
            projects.append(project)
    if set_self:
        self.projects = projects
    return projects

save(projects=None, directory=None)

Save a list of Project instances to a JSON file. Args: projects (List[Project]): A list of Project instances to be saved. directory (str, optional): The directory where the file will be saved. If None, uses the default directory.

Source code in ngwidgets/projects.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
def save(self, projects: List[Project] = None, directory: str = None):
    """
    Save a list of Project instances to a JSON file.
    Args:
        projects (List[Project]): A list of Project instances to be saved.
        directory (str, optional): The directory where the file will be saved. If None, uses the default directory.
    """
    if projects is None:
        projects = self.projects
    directory = Path(directory or self.default_directory)
    os.makedirs(directory, exist_ok=True)

    with open(self.file_path, "w", encoding="utf-8") as file:
        json.dump(
            [project.__dict__ for project in projects], file, indent=4, default=str
        )

sort_projects(projects, sort_key)

Sorts a list of projects based on the specified sort key, converting integers to fixed-length strings.

Parameters:

Name Type Description Default
projects list

List of Project instances.

required
sort_key str

Attribute name to sort the projects by.

required

Returns:

Name Type Description
list

Sorted list of projects.

Source code in ngwidgets/projects.py
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
def sort_projects(self, projects: List[Project], sort_key: str):
    """
    Sorts a list of projects based on the specified sort key, converting integers to fixed-length strings.

    Args:
        projects (list): List of Project instances.
        sort_key (str): Attribute name to sort the projects by.

    Returns:
        list: Sorted list of projects.
    """

    # Define the function to determine the sorting value
    def get_sort_value(proj):
        attr = getattr(proj, sort_key, None)

        # Handle None values; place them at the end of the sorted list
        if attr is None:
            return " "  # Assuming you want None values to appear last

        # Convert integers to zero-padded strings, and others to strings
        if isinstance(attr, int):
            return f"{attr:010d}"  # Zero-pad to 10 digits
        else:
            return str(attr).lower()

    # Determine if sorting should be in reverse
    reverse_sort = sort_key in ["stars", "downloads", "component_count"]

    return sorted(projects, key=get_sort_value, reverse=reverse_sort)

update(progress_bar=None, limit_github=None, limit_pypi=None)

Update the list of projects by retrieving potential projects from PyPI and GitHub based on the topic.

Parameters:

Name Type Description Default
progress_bar Optional[Progressbar]

A progress bar instance to update during the process.

None
limit_github Optional[int]

If set, limit the maximum number of GitHub projects to retrieve.

None
limit_pypi Optional[int]

If set, limit the maximum number of PyPI projects to retrieve.

None
Source code in ngwidgets/projects.py
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
def update(
    self,
    progress_bar: Optional[Progressbar] = None,
    limit_github: Optional[int] = None,
    limit_pypi: Optional[int] = None,
):
    """
    Update the list of projects by retrieving potential projects from PyPI and GitHub based on the topic.

    Args:
        progress_bar (Optional[Progressbar]): A progress bar instance to update during the process.
        limit_github (Optional[int]): If set, limit the maximum number of GitHub projects to retrieve.
        limit_pypi (Optional[int]): If set, limit the maximum number of PyPI projects to retrieve.
    """
    # Initialize progress bar if provided
    if progress_bar:
        cached_projects = self.load()
        progress_bar.total = len(cached_projects)
        progress_bar.reset()
        progress_bar.set_description("Updating projects")

    # pypi access
    pypi = PyPi()

    # Fetch projects from PyPI
    pypi_projects = pypi.search_projects(self.topic)
    # Apply limit to the PyPI projects
    if limit_pypi is not None:
        pypi_projects = pypi_projects[:limit_pypi]
    # Fetch repositories from GitHub
    github_access = GitHubAccess(self.default_directory)
    query = self.topic
    repo_dict = github_access.search_repositories(query)
    # Apply limit to the GitHub repositories
    if limit_github is not None:
        repo_dict = dict(list(repo_dict.items())[:limit_github])
    total = len(repo_dict) + len(pypi_projects)
    if progress_bar:
        progress_bar.total = total

    projects_by_github_url = self.get_github_projects(repo_dict, progress_bar)
    self.projects = list(projects_by_github_url.values())

    # Merge PyPI projects into the GitHub projects
    for pypi in pypi_projects:
        matched_project = None  # Reset for each PyPI project
        if pypi.github:
            for github_url in projects_by_github_url.keys():
                if pypi.github.startswith(github_url):
                    matched_project = projects_by_github_url[github_url]
            if matched_project:
                matched_project.merge_pypi(pypi)
            else:
                # we have github url but it was not in our search list
                # check the gitub repo for more details
                repo_name = self.extract_repo_name_from_url(pypi.github)
                if not repo_name:
                    raise ValueError(
                        f"Can't determine repo_name for {pypi.github} of pypi package {pypi.package}"
                    )
                # Create a Project instance from GitHub
                repo = github_access.github.get_repo(repo_name)
                github_comp = Project.from_github(repo)
                # Merge PyPI data into the newly created GitHub project
                github_comp.merge_pypi(pypi)
                self.projects.append(github_comp)
        else:
            # PyPI project without a GitHub URL
            self.projects.append(pypi)
        if progress_bar:
            progress_bar.update(1)
    # sort projects by name
    self.projects = sorted(
        self.projects, key=lambda comp: comp.name.lower() if comp.name else ""
    )
    self.last_update_time = datetime.now()

PyPi

Wrapper class for interacting with PyPI, including search functionality.

Source code in ngwidgets/projects.py
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
class PyPi:
    """
    Wrapper class for interacting with PyPI, including search functionality.
    """

    def __init__(self, debug: bool = False):
        self.base_url = "https://pypi.org/pypi"
        self.debug = debug
        self.cache_dir = Path.home() / ".pypi"
        self.cache_dir.mkdir(exist_ok=True)
        self.cache_file = self.cache_dir / "pypi-package-list.json"
        self.cache_ttl = 3600  # 1 hour

    def pypi_request(self, url: str, headers: Optional[Dict] = None) -> Optional[Dict]:
        """Make pypi request with simple file caching."""
        data = None
        if self.cache_file.exists():
            file_age = time.time() - self.cache_file.stat().st_mtime
            if file_age < self.cache_ttl:
                try:
                    with open(self.cache_file) as f:
                        data = json.load(f)
                except json.JSONDecodeError:
                    pass

        if not data:
            data = self._make_request(url, headers)
            if data:
                with open(self.cache_file, "w") as f:
                    json.dump(data, f)

        return data

    def _make_request(self, url: str, headers: Optional[Dict] = None) -> Optional[Dict]:
        """Make a request to PyPI API and return JSON response."""
        try:
            request = urllib.request.Request(url, headers=headers if headers else {})
            response = urllib.request.urlopen(request)
            if response.getcode() == 200:
                json_response = json.loads(response.read())
                return json_response
        except (urllib.error.URLError, json.JSONDecodeError) as e:
            if self.debug:
                print(f"Error in request to {url}: {e}")
        return None

    def get_package_info(self, package_name: str) -> Optional[Dict]:
        """Get detailed info for a specific package."""
        url = f"{self.base_url}/{package_name}/json"
        package_data = self._make_request(url)
        if package_data:
            package_data["package_url"] = f"https://pypi.org/project/{package_name}"
        return package_data

    def search_packages(self, term: str, limit: Optional[int] = None) -> List[Dict]:
        """Search packages using PyPI JSON API."""
        headers = {"Accept": "application/vnd.pypi.simple.v1+json"}
        url = "https://pypi.org/simple/"
        packages = []

        projects = self.pypi_request(url, headers)
        if projects:
            matched_projects = [
                p for p in projects["projects"] if term.lower() in p["name"].lower()
            ]
            if limit:
                matched_projects = matched_projects[:limit]

            for project in matched_projects:
                package_info = self.get_package_info(project["name"])
                if package_info:
                    packages.append(package_info)

        return packages

    def search_projects(self, term: str, limit: Optional[int] = None) -> List[Project]:
        """Get PyPI package info as Project instances."""
        package_dicts = self.search_packages(term, limit)
        projects = []
        for package in package_dicts:
            if package:
                project = Project.from_pypi(package)
                projects.append(project)
        return projects

get_package_info(package_name)

Get detailed info for a specific package.

Source code in ngwidgets/projects.py
700
701
702
703
704
705
706
def get_package_info(self, package_name: str) -> Optional[Dict]:
    """Get detailed info for a specific package."""
    url = f"{self.base_url}/{package_name}/json"
    package_data = self._make_request(url)
    if package_data:
        package_data["package_url"] = f"https://pypi.org/project/{package_name}"
    return package_data

pypi_request(url, headers=None)

Make pypi request with simple file caching.

Source code in ngwidgets/projects.py
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
def pypi_request(self, url: str, headers: Optional[Dict] = None) -> Optional[Dict]:
    """Make pypi request with simple file caching."""
    data = None
    if self.cache_file.exists():
        file_age = time.time() - self.cache_file.stat().st_mtime
        if file_age < self.cache_ttl:
            try:
                with open(self.cache_file) as f:
                    data = json.load(f)
            except json.JSONDecodeError:
                pass

    if not data:
        data = self._make_request(url, headers)
        if data:
            with open(self.cache_file, "w") as f:
                json.dump(data, f)

    return data

search_packages(term, limit=None)

Search packages using PyPI JSON API.

Source code in ngwidgets/projects.py
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
def search_packages(self, term: str, limit: Optional[int] = None) -> List[Dict]:
    """Search packages using PyPI JSON API."""
    headers = {"Accept": "application/vnd.pypi.simple.v1+json"}
    url = "https://pypi.org/simple/"
    packages = []

    projects = self.pypi_request(url, headers)
    if projects:
        matched_projects = [
            p for p in projects["projects"] if term.lower() in p["name"].lower()
        ]
        if limit:
            matched_projects = matched_projects[:limit]

        for project in matched_projects:
            package_info = self.get_package_info(project["name"])
            if package_info:
                packages.append(package_info)

    return packages

search_projects(term, limit=None)

Get PyPI package info as Project instances.

Source code in ngwidgets/projects.py
729
730
731
732
733
734
735
736
737
def search_projects(self, term: str, limit: Optional[int] = None) -> List[Project]:
    """Get PyPI package info as Project instances."""
    package_dicts = self.search_packages(term, limit)
    projects = []
    for package in package_dicts:
        if package:
            project = Project.from_pypi(package)
            projects.append(project)
    return projects

projects_view

Created on 2023-15-12

This module, developed as part of the ngwidgets package under the instruction of WF, provides classes for displaying software projects in a NiceGUI application. It defines the ProjectView class for rendering a single project and the ProjectsView class for managing a collection of ProjectView instances in a searchable and sortable card layout.

see https://github.com/WolfgangFahl/nicegui_widgets/issues/50

The implementation is inspired by a Streamlit application, as analyzed from a provided screenshot, which includes UI elements such as a date picker widget, package metadata display, and installation command. These will be recreated using NiceGUI's elements such as ui.date_picker(), ui.label(), ui.link(), ui.card(), ui.text(), and ui.input() for a similar user experience.

For details on the original Streamlit implementation, refer to: https://raw.githubusercontent.com/jrieke/projects-hub/main/streamlit_app.py

Prompts for LLM: - Incorporate the Project and Projects classes from ngwidgets into NiceGUI. - Implement the setup method for ProjectView to render project details in a UI card. - Implement the setup method for ProjectsView to manage a searchable and sortable display of projects. - Adapt the webserver class from ngwidgets to use ProjectsView for displaying projects.

Main author: OpenAI's language model (instructed by WF)

ProjectView

display a single project

Source code in ngwidgets/projects_view.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class ProjectView:
    """
    display a single project
    """

    def __init__(self, project: Project):
        self.project = project

    def setup(self, container) -> ui.card:
        """
        setup a card for my project
        """
        with container:
            self.card = ui.card()
            with self.card:
                with ui.row().classes("flex w-full items-center"):
                    # Title
                    title = f"{self.project.name}"
                    if self.project.version:
                        title = f"{title} - {self.project.version}"
                    ui.label(title).classes("text-2xl")
                    if self.project.stars:
                        # Flexible space to push stars to the right
                        ui.label("").classes("flex-grow")
                        star_rating = math.ceil(math.log10(self.project.stars + 0.5))
                        star_rating = min(star_rating, 5)
                        github_stars = f"{'⭐'*star_rating}{self.project.stars}"
                        ui.label(github_stars).classes("text-xl ml-auto")
                columns = 4 if self.project.components_url else 3
                self.card_grid = ui.grid(columns=columns)
                with self.card_grid:
                    if self.project.pypi:
                        pypi_icon = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/PyPI_logo.svg/64px-PyPI_logo.svg.png' alt='pypi' title='pypi'/>"
                        pypi_link = Link.create(
                            self.project.pypi, f"{pypi_icon}{self.project.package}"
                        )
                        html_markup = pypi_link
                        self.pypi_html = ui.html(html_markup)
                    if self.project.github:
                        github_icon = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Octicons-mark-github.svg/32px-Octicons-mark-github.svg.png' alt='github' title='github'/>"
                        github_name = self.project.github_repo_name
                        github_html_markup = f"{github_icon}{github_name}"
                        github_link = Link.create(
                            self.project.github, github_html_markup
                        )
                        html_markup = f"{github_link}"
                        self.github_html = ui.html(html_markup)
                        html_markup = ""
                        if self.project.github_author:
                            author = self.project.github_author
                            author_url = f"https://github.com/{author}"
                            if self.project.avatar:
                                avatar_icon = f"<img src='{self.project.avatar}' alt='{author}' title='{author}' style='width: 40px; height: 40px; border-radius: 50%;'/>"
                            else:
                                avatar_icon = author
                            author_link = Link.create(
                                author_url, f"{avatar_icon}{author}"
                            )
                            html_markup = f"{author_link}"
                        self.project_html = ui.html(html_markup)
                        # components (if any)
                        html_markup = ""
                        if self.project.components_url:
                            components = self.project.get_components()
                            components_count = len(
                                components.components
                            )  # Assuming get_components returns a list
                            components_icon = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Octicons-puzzle.svg/32px-Octicons-puzzle.svg.png' alt='components' title='components'/>"
                            components_restful_url = (
                                f"/components/{self.project.solution_id}"
                            )
                            components_link = Link.create(
                                components_restful_url, components_icon
                            )
                            html_markup += f" {components_link} {components_count}"
                            self.components_html = ui.html(html_markup)
                html_markup = ""
                if self.project.pypi:
                    if self.project.pypi_description:
                        html_markup = f"""<strong>{self.project.package}</strong>:
            <span>{self.project.pypi_description}</span>"""
                    inst_html = f"<pre>{self.project.install_instructions}</pre>"
                    html_markup = f"{html_markup}\n{inst_html}"

                self.desc_html = ui.html(html_markup)
            return self.card

setup(container)

setup a card for my project

Source code in ngwidgets/projects_view.py
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def setup(self, container) -> ui.card:
    """
    setup a card for my project
    """
    with container:
        self.card = ui.card()
        with self.card:
            with ui.row().classes("flex w-full items-center"):
                # Title
                title = f"{self.project.name}"
                if self.project.version:
                    title = f"{title} - {self.project.version}"
                ui.label(title).classes("text-2xl")
                if self.project.stars:
                    # Flexible space to push stars to the right
                    ui.label("").classes("flex-grow")
                    star_rating = math.ceil(math.log10(self.project.stars + 0.5))
                    star_rating = min(star_rating, 5)
                    github_stars = f"{'⭐'*star_rating}{self.project.stars}"
                    ui.label(github_stars).classes("text-xl ml-auto")
            columns = 4 if self.project.components_url else 3
            self.card_grid = ui.grid(columns=columns)
            with self.card_grid:
                if self.project.pypi:
                    pypi_icon = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/6/64/PyPI_logo.svg/64px-PyPI_logo.svg.png' alt='pypi' title='pypi'/>"
                    pypi_link = Link.create(
                        self.project.pypi, f"{pypi_icon}{self.project.package}"
                    )
                    html_markup = pypi_link
                    self.pypi_html = ui.html(html_markup)
                if self.project.github:
                    github_icon = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Octicons-mark-github.svg/32px-Octicons-mark-github.svg.png' alt='github' title='github'/>"
                    github_name = self.project.github_repo_name
                    github_html_markup = f"{github_icon}{github_name}"
                    github_link = Link.create(
                        self.project.github, github_html_markup
                    )
                    html_markup = f"{github_link}"
                    self.github_html = ui.html(html_markup)
                    html_markup = ""
                    if self.project.github_author:
                        author = self.project.github_author
                        author_url = f"https://github.com/{author}"
                        if self.project.avatar:
                            avatar_icon = f"<img src='{self.project.avatar}' alt='{author}' title='{author}' style='width: 40px; height: 40px; border-radius: 50%;'/>"
                        else:
                            avatar_icon = author
                        author_link = Link.create(
                            author_url, f"{avatar_icon}{author}"
                        )
                        html_markup = f"{author_link}"
                    self.project_html = ui.html(html_markup)
                    # components (if any)
                    html_markup = ""
                    if self.project.components_url:
                        components = self.project.get_components()
                        components_count = len(
                            components.components
                        )  # Assuming get_components returns a list
                        components_icon = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Octicons-puzzle.svg/32px-Octicons-puzzle.svg.png' alt='components' title='components'/>"
                        components_restful_url = (
                            f"/components/{self.project.solution_id}"
                        )
                        components_link = Link.create(
                            components_restful_url, components_icon
                        )
                        html_markup += f" {components_link} {components_count}"
                        self.components_html = ui.html(html_markup)
            html_markup = ""
            if self.project.pypi:
                if self.project.pypi_description:
                    html_markup = f"""<strong>{self.project.package}</strong>:
        <span>{self.project.pypi_description}</span>"""
                inst_html = f"<pre>{self.project.install_instructions}</pre>"
                html_markup = f"{html_markup}\n{inst_html}"

            self.desc_html = ui.html(html_markup)
        return self.card

ProjectsView

display the available projects as rows and columns sorted by user preference (default: stars) and allows to search/filter

Source code in ngwidgets/projects_view.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
class ProjectsView:
    """
    display the available projects as rows and columns
    sorted by user preference (default: stars) and allows to search/filter
    """

    def __init__(self, webserver: "InputWebserver", projects: Projects = None):
        """Initialize the ProjectsView with an optional collection of Projects.

        Args:
            webserver (InputWebserver): The webserver that serves the projects.
            projects (Projects): The collection of software projects. If None, projects are loaded from storage.
        """
        self.webserver = webserver
        if projects is None:
            projects = Projects(topic="nicegui")
            projects.load()
        self.projects = projects
        self.sorting = "stars"  # Set the default sorting method to "Stars"
        self.cards_container = None
        self.setup()

    def setup(self):
        """Set up the UI elements to render the collection of projects as a searchable and sortable card layout in NiceGUI."""
        with ui.row():
            self.last_update_label = ui.label()
            self.update_button = ui.button("Update", on_click=self.update_projects)
            self.update_last_update_label()
            self.progress_bar = NiceguiProgressbar(
                total=100, desc="updating projects", unit="projects"
            )

        with ui.row():
            # Filter input for searching projects
            self.filter_input = ui.input(
                placeholder="Search projects...", on_change=self.update_view
            )
        # Radio buttons for sorting
        sort_options = {
            "stars": "Stars",
            "name": "Name",
            "component_count": "Component Count",
            "github_owner": "Owner",
        }
        with ui.row():
            ui.label("Sort by:")
            self.sort_radio_group = (
                ui.radio(options=sort_options, on_change=self.update_view)
                .props("inline")
                .bind_value(self, "sorting")
            )

        # Project cards container
        self.cards_container = ui.grid(columns=4)
        self.views = {}
        # Initially display all projects
        self.update_view()

    def update_view(self):
        """Update the view to render the filtered and sorted projects."""
        if not self.cards_container:
            return
        search_term = self.filter_input.value.lower()
        if search_term:
            filtered_projects = [
                comp
                for comp in self.projects.projects
                if search_term in comp.name.lower()
            ]
        else:
            # Include all projects if search term is empty
            filtered_projects = self.projects.projects

        # Clear the current cards container
        self.cards_container.clear()
        if self.sorting:
            sorted_projects = self.projects.sort_projects(
                filtered_projects, self.sorting
            )
        else:
            sorted_projects = filtered_projects
        # Create a card for each project
        for project in sorted_projects:
            cv = ProjectView(project)
            self.views[project.name] = cv
            cv.setup(self.cards_container)

    async def update_projects(self, p):
        """
        update the projects
        """
        # avoid multiple background runs
        self.update_button.disable()
        ui.notify("Updating projects ... this might take a few seconds")

        await run.io_bound(self.projects.update, progress_bar=self.progress_bar)
        await run.io_bound(self.projects.save)

        # Notify the user after completion (optional)
        ui.notify("Projects updated successfully.")
        self.update_last_update_label()

    def update_last_update_label(self):
        """Update the label showing the last update time."""
        min_to_wait = 60  # Set the waiting time in minutes
        if self.projects.last_update_time:
            last_update_str = self.projects.last_update_time.strftime(
                "%Y-%m-%d %H:%M:%S"
            )
            self.last_update_label.set_text(f"Last Update: {last_update_str}")
            # Enable or disable the refresh button based on the GitHub API limits
            elapsed = datetime.now() - self.projects.last_update_time
            if elapsed < timedelta(minutes=min_to_wait):  # 60 calls per day?
                self.update_button.disable()
                # Calculate remaining minutes until the next update is possible
                remaining_time = timedelta(minutes=min_to_wait) - elapsed
                # Round up to the nearest whole minute
                minutes_until_next_update = math.ceil(
                    remaining_time.total_seconds() / 60
                )

                # Update the tooltip with the remaining minutes
                self.update_button.tooltip(
                    f"{minutes_until_next_update} min until enabled"
                )

            else:
                self.update_button.enable()
                self.update_button.tooltip("updating might take a few seconds")
        else:
            self.last_update_label.set_text("Last Update: Not yet updated")

__init__(webserver, projects=None)

Initialize the ProjectsView with an optional collection of Projects.

Parameters:

Name Type Description Default
webserver InputWebserver

The webserver that serves the projects.

required
projects Projects

The collection of software projects. If None, projects are loaded from storage.

None
Source code in ngwidgets/projects_view.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def __init__(self, webserver: "InputWebserver", projects: Projects = None):
    """Initialize the ProjectsView with an optional collection of Projects.

    Args:
        webserver (InputWebserver): The webserver that serves the projects.
        projects (Projects): The collection of software projects. If None, projects are loaded from storage.
    """
    self.webserver = webserver
    if projects is None:
        projects = Projects(topic="nicegui")
        projects.load()
    self.projects = projects
    self.sorting = "stars"  # Set the default sorting method to "Stars"
    self.cards_container = None
    self.setup()

setup()

Set up the UI elements to render the collection of projects as a searchable and sortable card layout in NiceGUI.

Source code in ngwidgets/projects_view.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def setup(self):
    """Set up the UI elements to render the collection of projects as a searchable and sortable card layout in NiceGUI."""
    with ui.row():
        self.last_update_label = ui.label()
        self.update_button = ui.button("Update", on_click=self.update_projects)
        self.update_last_update_label()
        self.progress_bar = NiceguiProgressbar(
            total=100, desc="updating projects", unit="projects"
        )

    with ui.row():
        # Filter input for searching projects
        self.filter_input = ui.input(
            placeholder="Search projects...", on_change=self.update_view
        )
    # Radio buttons for sorting
    sort_options = {
        "stars": "Stars",
        "name": "Name",
        "component_count": "Component Count",
        "github_owner": "Owner",
    }
    with ui.row():
        ui.label("Sort by:")
        self.sort_radio_group = (
            ui.radio(options=sort_options, on_change=self.update_view)
            .props("inline")
            .bind_value(self, "sorting")
        )

    # Project cards container
    self.cards_container = ui.grid(columns=4)
    self.views = {}
    # Initially display all projects
    self.update_view()

update_last_update_label()

Update the label showing the last update time.

Source code in ngwidgets/projects_view.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
def update_last_update_label(self):
    """Update the label showing the last update time."""
    min_to_wait = 60  # Set the waiting time in minutes
    if self.projects.last_update_time:
        last_update_str = self.projects.last_update_time.strftime(
            "%Y-%m-%d %H:%M:%S"
        )
        self.last_update_label.set_text(f"Last Update: {last_update_str}")
        # Enable or disable the refresh button based on the GitHub API limits
        elapsed = datetime.now() - self.projects.last_update_time
        if elapsed < timedelta(minutes=min_to_wait):  # 60 calls per day?
            self.update_button.disable()
            # Calculate remaining minutes until the next update is possible
            remaining_time = timedelta(minutes=min_to_wait) - elapsed
            # Round up to the nearest whole minute
            minutes_until_next_update = math.ceil(
                remaining_time.total_seconds() / 60
            )

            # Update the tooltip with the remaining minutes
            self.update_button.tooltip(
                f"{minutes_until_next_update} min until enabled"
            )

        else:
            self.update_button.enable()
            self.update_button.tooltip("updating might take a few seconds")
    else:
        self.last_update_label.set_text("Last Update: Not yet updated")

update_projects(p) async

update the projects

Source code in ngwidgets/projects_view.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
async def update_projects(self, p):
    """
    update the projects
    """
    # avoid multiple background runs
    self.update_button.disable()
    ui.notify("Updating projects ... this might take a few seconds")

    await run.io_bound(self.projects.update, progress_bar=self.progress_bar)
    await run.io_bound(self.projects.save)

    # Notify the user after completion (optional)
    ui.notify("Projects updated successfully.")
    self.update_last_update_label()

update_view()

Update the view to render the filtered and sorted projects.

Source code in ngwidgets/projects_view.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def update_view(self):
    """Update the view to render the filtered and sorted projects."""
    if not self.cards_container:
        return
    search_term = self.filter_input.value.lower()
    if search_term:
        filtered_projects = [
            comp
            for comp in self.projects.projects
            if search_term in comp.name.lower()
        ]
    else:
        # Include all projects if search term is empty
        filtered_projects = self.projects.projects

    # Clear the current cards container
    self.cards_container.clear()
    if self.sorting:
        sorted_projects = self.projects.sort_projects(
            filtered_projects, self.sorting
        )
    else:
        sorted_projects = filtered_projects
    # Create a card for each project
    for project in sorted_projects:
        cv = ProjectView(project)
        self.views[project.name] = cv
        cv.setup(self.cards_container)

scene_frame

Created on 2024-08-28

@author: wf

SceneFrame

a frame for a ui.scene with a potentially to be colors stl object see https://nicegui.io/documentation/scene

Source code in ngwidgets/scene_frame.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
class SceneFrame:
    """
    a frame for a ui.scene with a potentially to be colors stl object
    see https://nicegui.io/documentation/scene
    """

    def __init__(self, solution, stl_color: str = "#57B6A9"):
        """
        constructor
        """
        self.solution = solution
        self.stl_color = stl_color
        self.scene = None
        self.stl_objects = {}
        self.axes_view = None

    def setup_button_row(self):
        """
        setup my button row
        """
        with ui.row() as self.button_row:
            self.grid_button = self.solution.tool_button(
                "toggle grid",
                handler=self.toggle_grid,
                icon="grid_off",
                toggle_icon="grid_on",
            )
            self.axes_button = self.solution.tool_button(
                "toggle axes",
                icon="polyline",
                toggle_icon="square",
                handler=self.toggle_axes,
            )
            self.color_picker_button = ui.button(icon="colorize", color=self.stl_color)
            with self.color_picker_button:
                self.color_picker = ui.color_picker(on_pick=self.pick_color)
            self.color_picker_button.disable()

    async def pick_color(self, e: ColorPickEventArguments):
        """
        Asynchronously picks a color based on provided event arguments.

        This function changes the color of the 'color_picker_button' and the 'stl_object'
        according to the color specified in the event arguments.

        Args:
            e (ColorPickEventArguments): An object containing event-specific arguments.
                The 'color' attribute of this object specifies the color to be applied.

        Note:
            If 'stl_object' is None, the function will only change the color of 'color_picker_button'.
            Otherwise, it changes the color of both 'color_picker_button' and 'stl_object'.
        """
        self.color_picker_button.style(f"background-color:{e.color}!important")
        self.stl_color = e.color
        for stl_object in self.stl_objects.values():
            stl_object.material(f"{e.color}")
        pass

    async def toggle_axes(self):
        """
        toggle the axes of my scene
        """
        try:
            self.solution.toggle_icon(self.axes_button)
            new_state = not self.axes_view or not self.axes_view.axes_visible
            new_msg = "on" if new_state else "off"
            ui.notify(f"toggling axes {new_msg}")
            if self.axes_view is None:
                self.axes_view = AxesHelper(self.scene)
            else:
                self.axes_view.toggle_axes()
        except Exception as ex:
            self.solution.handle_exception(ex)

    async def toggle_grid(self, _ea):
        """
        toogle the grid of my scene
        """
        try:
            grid = self.scene._props["grid"]
            grid_str = "off" if grid else "on"
            grid_js = "false" if grid else "true"
            # try toggling grid
            ui.notify(f"setting grid to {grid_str}")
            grid = not grid
            # workaround according to https://github.com/zauberzeug/nicegui/discussions/1246
            js_cmd = f'scene_c{self.scene.id}.children.find(c => c.type === "GridHelper").visible = {grid_js}'
            await ui.run_javascript(js_cmd)
            self.scene._props["grid"] = grid
            self.scene.update()
            # try toggling icon
            self.solution.toggle_icon(self.grid_button)
        except Exception as ex:
            self.solution.handle_exception(ex)
        pass

    def clear(self):
        """
        Remove all objects from the scene.
        """
        with self.scene:
            self.scene.clear()

    def update(self):
        """
        update my scene
        """
        with self.scene:
            self.scene.update()

    def load_stl(
        self, stl_name: str, url: str, scale: float = 1.0, stl_color: str = "#57B6A9"
    ) -> object:
        """
        Loads an STL object into the scene, applies transformations, and sets its material.

        This method enables the color picker button, loads an STL file from the specified URL,
        applies a translation and scaling transformation, sets the object's name and material,
        and then returns the STL object.

        Args:
            stl_name (str): The name to assign to the STL object within the scene.
            url (str): The URL or path from which to load the STL file.
            scale (float, optional): The scale factor to apply to the STL object. Defaults to 1.0.
            stl_color (str, optional): the initial color to use for the stl object
        Returns:
            object: The loaded and transformed STL object within the scene.

        Raises:
            ValueError: If the STL file cannot be loaded from the specified URL.
        """
        self.color_picker_button.enable()
        with self.scene:
            stl_object = self.scene.stl(url).move(x=0.0).scale(scale)
            stl_object.name = stl_name
            if stl_color is None:
                stl_color = self.stl_color
            stl_object.material(stl_color)
            self.stl_objects[stl_name] = stl_object
            return stl_object

__init__(solution, stl_color='#57B6A9')

constructor

Source code in ngwidgets/scene_frame.py
19
20
21
22
23
24
25
26
27
def __init__(self, solution, stl_color: str = "#57B6A9"):
    """
    constructor
    """
    self.solution = solution
    self.stl_color = stl_color
    self.scene = None
    self.stl_objects = {}
    self.axes_view = None

clear()

Remove all objects from the scene.

Source code in ngwidgets/scene_frame.py
110
111
112
113
114
115
def clear(self):
    """
    Remove all objects from the scene.
    """
    with self.scene:
        self.scene.clear()

load_stl(stl_name, url, scale=1.0, stl_color='#57B6A9')

Loads an STL object into the scene, applies transformations, and sets its material.

This method enables the color picker button, loads an STL file from the specified URL, applies a translation and scaling transformation, sets the object's name and material, and then returns the STL object.

Parameters:

Name Type Description Default
stl_name str

The name to assign to the STL object within the scene.

required
url str

The URL or path from which to load the STL file.

required
scale float

The scale factor to apply to the STL object. Defaults to 1.0.

1.0
stl_color str

the initial color to use for the stl object

'#57B6A9'

Returns: object: The loaded and transformed STL object within the scene.

Raises:

Type Description
ValueError

If the STL file cannot be loaded from the specified URL.

Source code in ngwidgets/scene_frame.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def load_stl(
    self, stl_name: str, url: str, scale: float = 1.0, stl_color: str = "#57B6A9"
) -> object:
    """
    Loads an STL object into the scene, applies transformations, and sets its material.

    This method enables the color picker button, loads an STL file from the specified URL,
    applies a translation and scaling transformation, sets the object's name and material,
    and then returns the STL object.

    Args:
        stl_name (str): The name to assign to the STL object within the scene.
        url (str): The URL or path from which to load the STL file.
        scale (float, optional): The scale factor to apply to the STL object. Defaults to 1.0.
        stl_color (str, optional): the initial color to use for the stl object
    Returns:
        object: The loaded and transformed STL object within the scene.

    Raises:
        ValueError: If the STL file cannot be loaded from the specified URL.
    """
    self.color_picker_button.enable()
    with self.scene:
        stl_object = self.scene.stl(url).move(x=0.0).scale(scale)
        stl_object.name = stl_name
        if stl_color is None:
            stl_color = self.stl_color
        stl_object.material(stl_color)
        self.stl_objects[stl_name] = stl_object
        return stl_object

pick_color(e) async

Asynchronously picks a color based on provided event arguments.

This function changes the color of the 'color_picker_button' and the 'stl_object' according to the color specified in the event arguments.

Parameters:

Name Type Description Default
e ColorPickEventArguments

An object containing event-specific arguments. The 'color' attribute of this object specifies the color to be applied.

required
Note

If 'stl_object' is None, the function will only change the color of 'color_picker_button'. Otherwise, it changes the color of both 'color_picker_button' and 'stl_object'.

Source code in ngwidgets/scene_frame.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
async def pick_color(self, e: ColorPickEventArguments):
    """
    Asynchronously picks a color based on provided event arguments.

    This function changes the color of the 'color_picker_button' and the 'stl_object'
    according to the color specified in the event arguments.

    Args:
        e (ColorPickEventArguments): An object containing event-specific arguments.
            The 'color' attribute of this object specifies the color to be applied.

    Note:
        If 'stl_object' is None, the function will only change the color of 'color_picker_button'.
        Otherwise, it changes the color of both 'color_picker_button' and 'stl_object'.
    """
    self.color_picker_button.style(f"background-color:{e.color}!important")
    self.stl_color = e.color
    for stl_object in self.stl_objects.values():
        stl_object.material(f"{e.color}")
    pass

setup_button_row()

setup my button row

Source code in ngwidgets/scene_frame.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def setup_button_row(self):
    """
    setup my button row
    """
    with ui.row() as self.button_row:
        self.grid_button = self.solution.tool_button(
            "toggle grid",
            handler=self.toggle_grid,
            icon="grid_off",
            toggle_icon="grid_on",
        )
        self.axes_button = self.solution.tool_button(
            "toggle axes",
            icon="polyline",
            toggle_icon="square",
            handler=self.toggle_axes,
        )
        self.color_picker_button = ui.button(icon="colorize", color=self.stl_color)
        with self.color_picker_button:
            self.color_picker = ui.color_picker(on_pick=self.pick_color)
        self.color_picker_button.disable()

toggle_axes() async

toggle the axes of my scene

Source code in ngwidgets/scene_frame.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
async def toggle_axes(self):
    """
    toggle the axes of my scene
    """
    try:
        self.solution.toggle_icon(self.axes_button)
        new_state = not self.axes_view or not self.axes_view.axes_visible
        new_msg = "on" if new_state else "off"
        ui.notify(f"toggling axes {new_msg}")
        if self.axes_view is None:
            self.axes_view = AxesHelper(self.scene)
        else:
            self.axes_view.toggle_axes()
    except Exception as ex:
        self.solution.handle_exception(ex)

toggle_grid(_ea) async

toogle the grid of my scene

Source code in ngwidgets/scene_frame.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
async def toggle_grid(self, _ea):
    """
    toogle the grid of my scene
    """
    try:
        grid = self.scene._props["grid"]
        grid_str = "off" if grid else "on"
        grid_js = "false" if grid else "true"
        # try toggling grid
        ui.notify(f"setting grid to {grid_str}")
        grid = not grid
        # workaround according to https://github.com/zauberzeug/nicegui/discussions/1246
        js_cmd = f'scene_c{self.scene.id}.children.find(c => c.type === "GridHelper").visible = {grid_js}'
        await ui.run_javascript(js_cmd)
        self.scene._props["grid"] = grid
        self.scene.update()
        # try toggling icon
        self.solution.toggle_icon(self.grid_button)
    except Exception as ex:
        self.solution.handle_exception(ex)
    pass

update()

update my scene

Source code in ngwidgets/scene_frame.py
117
118
119
120
121
122
def update(self):
    """
    update my scene
    """
    with self.scene:
        self.scene.update()

short_url

Created on 2025-06-14

@author: wf

ShortUrl

A utility for mapping arbitrary code snippets to short base36 IDs, storing and retrieving them in a persistent directory.

Includes validation features: - maximum file size (in bytes) - required keywords (must be present) - blacklist keywords (must not be present)

Source code in ngwidgets/short_url.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class ShortUrl:
    """
    A utility for mapping arbitrary code snippets to short base36 IDs,
    storing and retrieving them in a persistent directory.

    Includes validation features:
    - maximum file size (in bytes)
    - required keywords (must be present)
    - blacklist keywords (must not be present)
    """

    def __init__(
        self,
        base_path: Path,
        suffix: str = ".txt",
        length: int = 8,
        max_size: int = 32 * 1024,
        required_keywords=None,
        blacklist=None,
        lenient: bool = False,
    ):
        """
        Initialize the ShortUrl utility.

        Args:
            base_path (Path): Directory to store code files.
            suffix (str): File extension to use (e.g. ".scad").
            length (int): Length of the generated base36 short ID.
            max_size (int): Maximum size in bytes of allowed code.
            required_keywords (List[str]): Keywords that must appear in the code.
            blacklist (List[str]): Keywords that must not appear in the code.
            lenient (bool): If True, do not raise on validation failure.
        """
        self.base_path = base_path
        self.suffix = suffix
        self.length = length
        self.max_size = max_size
        self.required_keywords = required_keywords or []
        self.blacklist = blacklist or []
        self.lenient = lenient
        self.base_path.mkdir(parents=True, exist_ok=True)

    def short_id_from_code(self, code: str) -> str:
        """
        Compute a base36-encoded short ID from the MD5 hash of the code.

        Args:
            code (str): The code string.

        Returns:
            str: base36 string of fixed length.
        """
        digest = hashlib.md5(code.encode("utf-8")).hexdigest()
        value = int(digest, 16)
        chars = "0123456789abcdefghijklmnopqrstuvwxyz"
        result = ""
        while value > 0:
            value, rem = divmod(value, 36)
            result = chars[rem] + result
        return result.zfill(self.length)[-self.length :]

    def path_for_code(self, code: str) -> Path:
        """Get file path for the given code."""
        return self.base_path / f"{self.short_id_from_code(code)}{self.suffix}"

    def path_for_id(self, short_id: str) -> Path:
        """Get file path for a given short ID."""
        return self.base_path / f"{short_id}{self.suffix}"

    def file_exists(self, short_id: str) -> bool:
        """Check whether a file exists for the given short ID."""
        return self.path_for_id(short_id).exists()

    def validate_code(self, code: str) -> str:
        """
        Validate the code against all constraints.

        Enforces:
        - maximum size (in bytes)
        - blacklist exclusion
        - presence of all required keywords

        Returns:
            str: None if valid, error message otherwise or raises ValueError if lenient is False.
        """
        msg = None
        size = len(code.encode("utf-8"))
        if size > self.max_size:
            msg = f"Code size {size} exceeds maximum allowed size of {self.max_size} bytes"
        if msg is None and self.blacklist:
            for bad in self.blacklist:
                if bad in code:
                    msg = f"Code contains blacklisted content: '{bad}'"
                    break
        if msg is None and self.required_keywords:
            missing = [req for req in self.required_keywords if req not in code]
            if missing:
                msg = f"Code is missing required keywords: {missing}"

        if not self.lenient and msg:
            raise ValueError(msg)

        return msg

    def save(self, code: str, with_validate: bool = True) -> str:
        """
        Save the code to a file named after its short ID.

        Args:
            code (str): The code content to save.
            with_validate (bool): Whether to validate the code before saving.

        Returns:
            str: The short ID if saved or already exists.

        Raises:
            ValueError: if validation fails and lenient is False.
        """
        short_id = self.short_id_from_code(code)
        path = self.path_for_id(short_id)
        err_msg = None
        if with_validate:
            err_msg = self.validate_code(code)
        if not err_msg and not path.exists():
            path.write_text(code, encoding="utf-8")
        return short_id

    def load(self, short_id: str) -> str:
        """
        Load code content by its short ID.

        Args:
            short_id (str): The short ID.

        Returns:
            str: Loaded code content.

        Raises:
            FileNotFoundError: If the file does not exist.
        """
        path = self.path_for_id(short_id)
        if not path.exists():
            raise FileNotFoundError(f"No code found for ID {short_id}")
        return path.read_text(encoding="utf-8")

__init__(base_path, suffix='.txt', length=8, max_size=32 * 1024, required_keywords=None, blacklist=None, lenient=False)

Initialize the ShortUrl utility.

Parameters:

Name Type Description Default
base_path Path

Directory to store code files.

required
suffix str

File extension to use (e.g. ".scad").

'.txt'
length int

Length of the generated base36 short ID.

8
max_size int

Maximum size in bytes of allowed code.

32 * 1024
required_keywords List[str]

Keywords that must appear in the code.

None
blacklist List[str]

Keywords that must not appear in the code.

None
lenient bool

If True, do not raise on validation failure.

False
Source code in ngwidgets/short_url.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def __init__(
    self,
    base_path: Path,
    suffix: str = ".txt",
    length: int = 8,
    max_size: int = 32 * 1024,
    required_keywords=None,
    blacklist=None,
    lenient: bool = False,
):
    """
    Initialize the ShortUrl utility.

    Args:
        base_path (Path): Directory to store code files.
        suffix (str): File extension to use (e.g. ".scad").
        length (int): Length of the generated base36 short ID.
        max_size (int): Maximum size in bytes of allowed code.
        required_keywords (List[str]): Keywords that must appear in the code.
        blacklist (List[str]): Keywords that must not appear in the code.
        lenient (bool): If True, do not raise on validation failure.
    """
    self.base_path = base_path
    self.suffix = suffix
    self.length = length
    self.max_size = max_size
    self.required_keywords = required_keywords or []
    self.blacklist = blacklist or []
    self.lenient = lenient
    self.base_path.mkdir(parents=True, exist_ok=True)

file_exists(short_id)

Check whether a file exists for the given short ID.

Source code in ngwidgets/short_url.py
80
81
82
def file_exists(self, short_id: str) -> bool:
    """Check whether a file exists for the given short ID."""
    return self.path_for_id(short_id).exists()

load(short_id)

Load code content by its short ID.

Parameters:

Name Type Description Default
short_id str

The short ID.

required

Returns:

Name Type Description
str str

Loaded code content.

Raises:

Type Description
FileNotFoundError

If the file does not exist.

Source code in ngwidgets/short_url.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def load(self, short_id: str) -> str:
    """
    Load code content by its short ID.

    Args:
        short_id (str): The short ID.

    Returns:
        str: Loaded code content.

    Raises:
        FileNotFoundError: If the file does not exist.
    """
    path = self.path_for_id(short_id)
    if not path.exists():
        raise FileNotFoundError(f"No code found for ID {short_id}")
    return path.read_text(encoding="utf-8")

path_for_code(code)

Get file path for the given code.

Source code in ngwidgets/short_url.py
72
73
74
def path_for_code(self, code: str) -> Path:
    """Get file path for the given code."""
    return self.base_path / f"{self.short_id_from_code(code)}{self.suffix}"

path_for_id(short_id)

Get file path for a given short ID.

Source code in ngwidgets/short_url.py
76
77
78
def path_for_id(self, short_id: str) -> Path:
    """Get file path for a given short ID."""
    return self.base_path / f"{short_id}{self.suffix}"

save(code, with_validate=True)

Save the code to a file named after its short ID.

Parameters:

Name Type Description Default
code str

The code content to save.

required
with_validate bool

Whether to validate the code before saving.

True

Returns:

Name Type Description
str str

The short ID if saved or already exists.

Raises:

Type Description
ValueError

if validation fails and lenient is False.

Source code in ngwidgets/short_url.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def save(self, code: str, with_validate: bool = True) -> str:
    """
    Save the code to a file named after its short ID.

    Args:
        code (str): The code content to save.
        with_validate (bool): Whether to validate the code before saving.

    Returns:
        str: The short ID if saved or already exists.

    Raises:
        ValueError: if validation fails and lenient is False.
    """
    short_id = self.short_id_from_code(code)
    path = self.path_for_id(short_id)
    err_msg = None
    if with_validate:
        err_msg = self.validate_code(code)
    if not err_msg and not path.exists():
        path.write_text(code, encoding="utf-8")
    return short_id

short_id_from_code(code)

Compute a base36-encoded short ID from the MD5 hash of the code.

Parameters:

Name Type Description Default
code str

The code string.

required

Returns:

Name Type Description
str str

base36 string of fixed length.

Source code in ngwidgets/short_url.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def short_id_from_code(self, code: str) -> str:
    """
    Compute a base36-encoded short ID from the MD5 hash of the code.

    Args:
        code (str): The code string.

    Returns:
        str: base36 string of fixed length.
    """
    digest = hashlib.md5(code.encode("utf-8")).hexdigest()
    value = int(digest, 16)
    chars = "0123456789abcdefghijklmnopqrstuvwxyz"
    result = ""
    while value > 0:
        value, rem = divmod(value, 36)
        result = chars[rem] + result
    return result.zfill(self.length)[-self.length :]

validate_code(code)

Validate the code against all constraints.

Enforces: - maximum size (in bytes) - blacklist exclusion - presence of all required keywords

Returns:

Name Type Description
str str

None if valid, error message otherwise or raises ValueError if lenient is False.

Source code in ngwidgets/short_url.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def validate_code(self, code: str) -> str:
    """
    Validate the code against all constraints.

    Enforces:
    - maximum size (in bytes)
    - blacklist exclusion
    - presence of all required keywords

    Returns:
        str: None if valid, error message otherwise or raises ValueError if lenient is False.
    """
    msg = None
    size = len(code.encode("utf-8"))
    if size > self.max_size:
        msg = f"Code size {size} exceeds maximum allowed size of {self.max_size} bytes"
    if msg is None and self.blacklist:
        for bad in self.blacklist:
            if bad in code:
                msg = f"Code contains blacklisted content: '{bad}'"
                break
    if msg is None and self.required_keywords:
        missing = [req for req in self.required_keywords if req not in code]
        if missing:
            msg = f"Code is missing required keywords: {missing}"

    if not self.lenient and msg:
        raise ValueError(msg)

    return msg

sso_users_solution

Created on 2025-02-15

@author: wf

SsoSolution

Bases: WebSolution

Encapsulates SSO authentication setup and user handling

Source code in ngwidgets/sso_users_solution.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
class SsoSolution(WebSolution):
    """
    Encapsulates SSO authentication setup and user handling
    """

    def __init__(
        self, webserver: NiceGuiWebserver, client: Client = None, credentials_path=None
    ):
        """
        Initialize SSO authentication

        Args:
            webserver: The webserver instance to set up SSO for
            client: The client instance (can be None for non-client specific solutions)
            credentials_path: Optional path to the SSO credentials file
        """
        super().__init__(
            webserver, client
        )  # Call parent constructor with both required parameters
        self.users = Sso_Users(
            webserver.config.short_name, credentials_path=credentials_path
        )

        self.login = Login(webserver, self.users)

    def get_user(self) -> User:
        """
        Get the current authenticated user

        Returns:
            User: The current user object or None if not authenticated
        """
        user = None
        username = self.login.get_username()
        if username and self.users.is_available:
            user = self.users.sso.get_user(username)
        return user

    def get_user_display_name(self) -> str:
        """
        Get the display name for the current user with admin indicator

        Returns:
            str: Username with optional admin indicator
        """
        username = self.login.get_username()
        if username is None:
            username = "?"
        user = self.get_user()
        admin_flag = "🔑" if user and user.is_admin else ""
        user_display = f"{username}{admin_flag}"
        return user_display

    async def logout(self):
        """Handle user logout"""
        await self.login.logout()

    def as_html(self) -> str:
        """
        Get the user details as HTML markup

        Returns:
            str: HTML markup for the user details or error message
        """
        user = self.get_user()
        html_markup = "<h1>User Details</h1>"
        if not user:
            html_markup += "<p>No user logged in</p>"
        else:
            html_markup += f"""
            <p><strong>ID:</strong> {user.id}</p>
            <p><strong>Name:</strong> {user.name}</p>
            <p><strong>Real Name:</strong> {user.real_name}</p>
            <p><strong>Email:</strong> {user.email}</p>
            <p><strong>Edit Count:</strong> {user.editcount}</p>
            """
        return html_markup

    async def show_login(self):
        """Show the login page"""
        await self.login.login(self)

    async def show_user_details(self):
        """Show the user details page"""

        def show():
            self.logout_button = ui.button(
                "logout", icon="logout", on_click=self.logout
            )
            ui.html(self.as_html())

        await self.setup_content_div(show)

    def configure_menu(self):
        """Configure the user menu"""
        display_name = self.get_user_display_name()
        self.link_button(display_name, "/user", "person")
        # make sure the link exists
        self.register_pages()

    def register_pages(self):
        """Register the SSO-related pages"""

        @ui.page("/user")
        async def show_user(client: Client):
            if not self.login.authenticated():
                return RedirectResponse("/login")
            return await self.webserver.execute_action(
                client,
                solution_class=SsoSolution,
                wanted_action=SsoSolution.show_user_details,
            )

        @ui.page("/login")
        async def login(client: Client):
            return await self.webserver.execute_action(
                client, solution_class=SsoSolution, wanted_action=SsoSolution.show_login
            )

__init__(webserver, client=None, credentials_path=None)

Initialize SSO authentication

Parameters:

Name Type Description Default
webserver NiceGuiWebserver

The webserver instance to set up SSO for

required
client Client

The client instance (can be None for non-client specific solutions)

None
credentials_path

Optional path to the SSO credentials file

None
Source code in ngwidgets/sso_users_solution.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def __init__(
    self, webserver: NiceGuiWebserver, client: Client = None, credentials_path=None
):
    """
    Initialize SSO authentication

    Args:
        webserver: The webserver instance to set up SSO for
        client: The client instance (can be None for non-client specific solutions)
        credentials_path: Optional path to the SSO credentials file
    """
    super().__init__(
        webserver, client
    )  # Call parent constructor with both required parameters
    self.users = Sso_Users(
        webserver.config.short_name, credentials_path=credentials_path
    )

    self.login = Login(webserver, self.users)

as_html()

Get the user details as HTML markup

Returns:

Name Type Description
str str

HTML markup for the user details or error message

Source code in ngwidgets/sso_users_solution.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def as_html(self) -> str:
    """
    Get the user details as HTML markup

    Returns:
        str: HTML markup for the user details or error message
    """
    user = self.get_user()
    html_markup = "<h1>User Details</h1>"
    if not user:
        html_markup += "<p>No user logged in</p>"
    else:
        html_markup += f"""
        <p><strong>ID:</strong> {user.id}</p>
        <p><strong>Name:</strong> {user.name}</p>
        <p><strong>Real Name:</strong> {user.real_name}</p>
        <p><strong>Email:</strong> {user.email}</p>
        <p><strong>Edit Count:</strong> {user.editcount}</p>
        """
    return html_markup

configure_menu()

Configure the user menu

Source code in ngwidgets/sso_users_solution.py
109
110
111
112
113
114
def configure_menu(self):
    """Configure the user menu"""
    display_name = self.get_user_display_name()
    self.link_button(display_name, "/user", "person")
    # make sure the link exists
    self.register_pages()

get_user()

Get the current authenticated user

Returns:

Name Type Description
User User

The current user object or None if not authenticated

Source code in ngwidgets/sso_users_solution.py
41
42
43
44
45
46
47
48
49
50
51
52
def get_user(self) -> User:
    """
    Get the current authenticated user

    Returns:
        User: The current user object or None if not authenticated
    """
    user = None
    username = self.login.get_username()
    if username and self.users.is_available:
        user = self.users.sso.get_user(username)
    return user

get_user_display_name()

Get the display name for the current user with admin indicator

Returns:

Name Type Description
str str

Username with optional admin indicator

Source code in ngwidgets/sso_users_solution.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def get_user_display_name(self) -> str:
    """
    Get the display name for the current user with admin indicator

    Returns:
        str: Username with optional admin indicator
    """
    username = self.login.get_username()
    if username is None:
        username = "?"
    user = self.get_user()
    admin_flag = "🔑" if user and user.is_admin else ""
    user_display = f"{username}{admin_flag}"
    return user_display

logout() async

Handle user logout

Source code in ngwidgets/sso_users_solution.py
69
70
71
async def logout(self):
    """Handle user logout"""
    await self.login.logout()

register_pages()

Register the SSO-related pages

Source code in ngwidgets/sso_users_solution.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def register_pages(self):
    """Register the SSO-related pages"""

    @ui.page("/user")
    async def show_user(client: Client):
        if not self.login.authenticated():
            return RedirectResponse("/login")
        return await self.webserver.execute_action(
            client,
            solution_class=SsoSolution,
            wanted_action=SsoSolution.show_user_details,
        )

    @ui.page("/login")
    async def login(client: Client):
        return await self.webserver.execute_action(
            client, solution_class=SsoSolution, wanted_action=SsoSolution.show_login
        )

show_login() async

Show the login page

Source code in ngwidgets/sso_users_solution.py
94
95
96
async def show_login(self):
    """Show the login page"""
    await self.login.login(self)

show_user_details() async

Show the user details page

Source code in ngwidgets/sso_users_solution.py
 98
 99
100
101
102
103
104
105
106
107
async def show_user_details(self):
    """Show the user details page"""

    def show():
        self.logout_button = ui.button(
            "logout", icon="logout", on_click=self.logout
        )
        ui.html(self.as_html())

    await self.setup_content_div(show)

task_runner

Created on 2025-03-09

@author: wf

TaskRunner

A robust background task runner that supports: - Explicit async, blocking, or async-wrapped-blocking modes - Enforces correct usage - Supports progress bar feedback - Enforces timeouts and cancellation - Returns task references for external cancellation

Source code in ngwidgets/task_runner.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
class TaskRunner:
    """
    A robust background task runner that supports:
    - Explicit async, blocking, or async-wrapped-blocking modes
    - Enforces correct usage
    - Supports progress bar feedback
    - Enforces timeouts and cancellation
    - Returns task references for external cancellation
    """

    def __init__(self, timeout: float = 20.0, progress: Optional[Progressbar] = None):
        self.task = None
        self.timeout = timeout
        self.progress = progress
        self.log = Log()
        self.start_time = None
        self.stop_time = None
        self.task_name = "?"

    def set_name(self, func: Callable):
        """Set task name from function"""
        self.task_name = getattr(func, "__name__", "?")

    def get_status(self) -> str:
        """Get formatted status string with timing information"""
        if not self.start_time:
            status = "Ready"
        else:
            elapsed = self.get_elapsed_time()
            start_str = time.strftime("%H:%M:%S", time.localtime(self.start_time))

            if self.is_running():
                status = (
                    f'Running "{self.task_name}" for {elapsed:.1f}s since {start_str}'
                )
            elif self.stop_time:
                stop_str = time.strftime("%H:%M:%S", time.localtime(self.stop_time))
                status = f'Completed "{self.task_name}" in {elapsed:.1f}s ({start_str}-{stop_str})'
            else:
                status = f'Interrupted "{self.task_name}" after {elapsed:.1f}s from {start_str}'

        return status

    def get_elapsed_time(self) -> float:
        """Get elapsed time in seconds since task started"""
        elapsed = 0.0
        if self.start_time:
            if self.is_running():
                elapsed = time.time() - self.start_time
            elif self.stop_time:
                elapsed = self.stop_time - self.start_time
        return elapsed

    def cancel_running(self):
        """🆕 Cancel currently running task and record timing"""
        if self.task and not self.task.done():
            try:
                self.task.cancel()
                self.log.log("🔄", "cancel", f'Cancelled task "{self.task_name}"')
            except Exception as ex:
                self.log.log("⚠️", "cancel_error", f"Error cancelling task: {ex}")

        if self.progress:
            self.progress.reset()

        # 🆕 Record stop time when cancelled (instead of resetting timing)
        if self.start_time and not self.stop_time:
            self.stop_time = time.time()

    def is_running(self) -> bool:
        """Check if task is currently running"""
        running = self.task and not self.task.done()
        return running

    def run(self, func: Callable, *args, **kwargs):
        """
        Automatically detect function type and run appropriately.

        Args:
            func: async or sync function
            *args, **kwargs: arguments to pass to func

        Returns:
            asyncio.Task: The created background task
        """
        if inspect.iscoroutinefunction(func):
            return self.run_async(func, *args, **kwargs)
        else:
            return self.run_blocking(func, *args, **kwargs)

    def run_blocking(
        self,
        blocking_func: Callable,
        *args,
        on_result: Optional[Callable] = None,
        **kwargs,
    ):
        """
        Run a blocking (sync) function via asyncio.to_thread.

        Args:
            blocking_func: a regular function doing I/O or CPU-heavy work
            on_result: called with the return value of blocking_func when given
            *args, **kwargs: arguments to pass to blocking_func

        Returns:
            asyncio.Task: The created background task
        """
        if inspect.iscoroutinefunction(blocking_func) or inspect.iscoroutine(
            blocking_func
        ):
            raise TypeError("run_blocking expects a sync function, not async.")
        self.set_name(blocking_func)

        async def wrapper():
            # Use nicegui managed threads
            result = await run.io_bound(blocking_func, *args, **kwargs)
            if on_result:
                on_result(result)

        return self._start(wrapper)

    def run_async_wrapping_blocking(
        self,
        coro_func: Callable[[], asyncio.Future],
        on_result: Optional[Callable] = None,
    ):
        """
        Run an async function that internally handles blocking with to_thread.

        Args:
            coro_func: async function doing await to_thread(blocking_func)
            on_result: called with the return value of coro_func when given

        Returns:
            asyncio.Task: The created background task
        """
        if not inspect.iscoroutinefunction(coro_func):
            raise TypeError(
                "run_async_wrapping_blocking expects an async def function."
            )
        self.set_name(coro_func)
        return self._start(coro_func)

    def run_async(self, coro_func: Callable[..., asyncio.Future], *args, **kwargs):
        """
        Run a pure async function in the background.

        Args:
            coro_func: a non-blocking async function
            *args, **kwargs: arguments to pass to coro_func

        Returns:
            asyncio.Task: The created background task
        """
        if not inspect.iscoroutinefunction(coro_func):
            raise TypeError("run_async expects an async def function (not called yet).")
        self.set_name(coro_func)
        return self._start(coro_func, *args, **kwargs)

    def _start(
        self,
        coro_func: Callable[..., asyncio.Future],
        *args,
        on_result: Optional[Callable] = None,
        **kwargs,
    ):
        """
        🆕 Internal method to start a task with proper error handling and timing.

        Returns:
            asyncio.Task: The created background task
        """
        self.cancel_running()
        self.start_time = time.time()
        self.stop_time = None  # 🆕 Reset stop time on new task

        async def wrapped():
            try:
                if self.progress:
                    self.progress.reset()
                    self.progress.set_description("Working...")
                result = await asyncio.wait_for(
                    coro_func(*args, **kwargs), timeout=self.timeout
                )
                if on_result:
                    on_result(result)
            except asyncio.TimeoutError:
                self.log.log(
                    "❌",
                    "timeout",
                    f'Task "{self.task_name}" exceeded {self.timeout}s timeout — possible blocking code?',
                )
            except asyncio.CancelledError:
                self.log.log(
                    "⚠️", "cancelled", f'Task "{self.task_name}" was cancelled.'
                )
                raise  # 🆕 Re-raise to properly propagate cancellation
            except Exception as ex:
                self.log.log("❌", "exception", f'Task "{self.task_name}": {str(ex)}')
                raise  # 🆕 Re-raise to allow caller to handle
            finally:
                if self.progress:
                    self.progress.update_value(self.progress.total)
                self.stop_time = time.time()

        self.task = background_tasks.create(wrapped())
        return self.task  # 🆕 Return task reference for external cancellation

cancel_running()

🆕 Cancel currently running task and record timing

Source code in ngwidgets/task_runner.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def cancel_running(self):
    """🆕 Cancel currently running task and record timing"""
    if self.task and not self.task.done():
        try:
            self.task.cancel()
            self.log.log("🔄", "cancel", f'Cancelled task "{self.task_name}"')
        except Exception as ex:
            self.log.log("⚠️", "cancel_error", f"Error cancelling task: {ex}")

    if self.progress:
        self.progress.reset()

    # 🆕 Record stop time when cancelled (instead of resetting timing)
    if self.start_time and not self.stop_time:
        self.stop_time = time.time()

get_elapsed_time()

Get elapsed time in seconds since task started

Source code in ngwidgets/task_runner.py
61
62
63
64
65
66
67
68
69
def get_elapsed_time(self) -> float:
    """Get elapsed time in seconds since task started"""
    elapsed = 0.0
    if self.start_time:
        if self.is_running():
            elapsed = time.time() - self.start_time
        elif self.stop_time:
            elapsed = self.stop_time - self.start_time
    return elapsed

get_status()

Get formatted status string with timing information

Source code in ngwidgets/task_runner.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def get_status(self) -> str:
    """Get formatted status string with timing information"""
    if not self.start_time:
        status = "Ready"
    else:
        elapsed = self.get_elapsed_time()
        start_str = time.strftime("%H:%M:%S", time.localtime(self.start_time))

        if self.is_running():
            status = (
                f'Running "{self.task_name}" for {elapsed:.1f}s since {start_str}'
            )
        elif self.stop_time:
            stop_str = time.strftime("%H:%M:%S", time.localtime(self.stop_time))
            status = f'Completed "{self.task_name}" in {elapsed:.1f}s ({start_str}-{stop_str})'
        else:
            status = f'Interrupted "{self.task_name}" after {elapsed:.1f}s from {start_str}'

    return status

is_running()

Check if task is currently running

Source code in ngwidgets/task_runner.py
87
88
89
90
def is_running(self) -> bool:
    """Check if task is currently running"""
    running = self.task and not self.task.done()
    return running

run(func, *args, **kwargs)

Automatically detect function type and run appropriately.

Parameters:

Name Type Description Default
func Callable

async or sync function

required
*args, **kwargs

arguments to pass to func

required

Returns:

Type Description

asyncio.Task: The created background task

Source code in ngwidgets/task_runner.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def run(self, func: Callable, *args, **kwargs):
    """
    Automatically detect function type and run appropriately.

    Args:
        func: async or sync function
        *args, **kwargs: arguments to pass to func

    Returns:
        asyncio.Task: The created background task
    """
    if inspect.iscoroutinefunction(func):
        return self.run_async(func, *args, **kwargs)
    else:
        return self.run_blocking(func, *args, **kwargs)

run_async(coro_func, *args, **kwargs)

Run a pure async function in the background.

Parameters:

Name Type Description Default
coro_func Callable[..., Future]

a non-blocking async function

required
*args, **kwargs

arguments to pass to coro_func

required

Returns:

Type Description

asyncio.Task: The created background task

Source code in ngwidgets/task_runner.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def run_async(self, coro_func: Callable[..., asyncio.Future], *args, **kwargs):
    """
    Run a pure async function in the background.

    Args:
        coro_func: a non-blocking async function
        *args, **kwargs: arguments to pass to coro_func

    Returns:
        asyncio.Task: The created background task
    """
    if not inspect.iscoroutinefunction(coro_func):
        raise TypeError("run_async expects an async def function (not called yet).")
    self.set_name(coro_func)
    return self._start(coro_func, *args, **kwargs)

run_async_wrapping_blocking(coro_func, on_result=None)

Run an async function that internally handles blocking with to_thread.

Parameters:

Name Type Description Default
coro_func Callable[[], Future]

async function doing await to_thread(blocking_func)

required
on_result Optional[Callable]

called with the return value of coro_func when given

None

Returns:

Type Description

asyncio.Task: The created background task

Source code in ngwidgets/task_runner.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def run_async_wrapping_blocking(
    self,
    coro_func: Callable[[], asyncio.Future],
    on_result: Optional[Callable] = None,
):
    """
    Run an async function that internally handles blocking with to_thread.

    Args:
        coro_func: async function doing await to_thread(blocking_func)
        on_result: called with the return value of coro_func when given

    Returns:
        asyncio.Task: The created background task
    """
    if not inspect.iscoroutinefunction(coro_func):
        raise TypeError(
            "run_async_wrapping_blocking expects an async def function."
        )
    self.set_name(coro_func)
    return self._start(coro_func)

run_blocking(blocking_func, *args, on_result=None, **kwargs)

Run a blocking (sync) function via asyncio.to_thread.

Parameters:

Name Type Description Default
blocking_func Callable

a regular function doing I/O or CPU-heavy work

required
on_result Optional[Callable]

called with the return value of blocking_func when given

None
*args, **kwargs

arguments to pass to blocking_func

required

Returns:

Type Description

asyncio.Task: The created background task

Source code in ngwidgets/task_runner.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def run_blocking(
    self,
    blocking_func: Callable,
    *args,
    on_result: Optional[Callable] = None,
    **kwargs,
):
    """
    Run a blocking (sync) function via asyncio.to_thread.

    Args:
        blocking_func: a regular function doing I/O or CPU-heavy work
        on_result: called with the return value of blocking_func when given
        *args, **kwargs: arguments to pass to blocking_func

    Returns:
        asyncio.Task: The created background task
    """
    if inspect.iscoroutinefunction(blocking_func) or inspect.iscoroutine(
        blocking_func
    ):
        raise TypeError("run_blocking expects a sync function, not async.")
    self.set_name(blocking_func)

    async def wrapper():
        # Use nicegui managed threads
        result = await run.io_bound(blocking_func, *args, **kwargs)
        if on_result:
            on_result(result)

    return self._start(wrapper)

set_name(func)

Set task name from function

Source code in ngwidgets/task_runner.py
37
38
39
def set_name(self, func: Callable):
    """Set task name from function"""
    self.task_name = getattr(func, "__name__", "?")

test_base_webserver

Refactored on 2025-05-19

@author: wf

BaseWebserverTest

Bases: Basetest

Common base class for both TestClient and real HTTP testing scenarios

This provides a unified interface for testing web server responses whether using FastAPI TestClient or real HTTP requests.

Source code in ngwidgets/test_base_webserver.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
class BaseWebserverTest(Basetest):
    """
    Common base class for both TestClient and real HTTP testing scenarios

    This provides a unified interface for testing web server responses
    whether using FastAPI TestClient or real HTTP requests.
    """

    def setUp(self, debug=False, profile=True):
        """Set up the test environment"""
        super().setUp(debug=debug, profile=profile)

    def get_response(self, path: str, expected_status_code: int = 200) -> Any:
        """
        Get a response for the given path and verify its status code

        Args:
            path: The path to request
            expected_status_code: The expected HTTP status code

        Returns:
            The response object
        """
        raise NotImplementedError("Subclasses must implement get_response method")

    def get_html(self, path: str, expected_status_code: int = 200) -> str:
        """
        Get HTML content for the given path

        Args:
            path: The path to request
            expected_status_code: The expected HTTP status code

        Returns:
            The HTML content as a string
        """
        response = self.get_response(path, expected_status_code)
        if hasattr(response, "content"):
            html = response.content.decode()

            if self.debug:
                print(html)
            return html
        return ""

    def get_json(self, path: str, expected_status_code: int = 200) -> Any:
        """
        Get JSON content for the given path

        Args:
            path: The path to request
            expected_status_code: The expected HTTP status code

        Returns:
            The parsed JSON data
        """
        response = self.get_response(path, expected_status_code)
        try:
            if hasattr(response, "json"):
                # Both requests.Response and TestClient responses have a json method
                return response.json()
            else:
                # Fallback
                return json.loads(response.content)
        except json.JSONDecodeError as e:
            self.fail(f"Failed to decode JSON for request {path}: {str(e)}")

get_html(path, expected_status_code=200)

Get HTML content for the given path

Parameters:

Name Type Description Default
path str

The path to request

required
expected_status_code int

The expected HTTP status code

200

Returns:

Type Description
str

The HTML content as a string

Source code in ngwidgets/test_base_webserver.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def get_html(self, path: str, expected_status_code: int = 200) -> str:
    """
    Get HTML content for the given path

    Args:
        path: The path to request
        expected_status_code: The expected HTTP status code

    Returns:
        The HTML content as a string
    """
    response = self.get_response(path, expected_status_code)
    if hasattr(response, "content"):
        html = response.content.decode()

        if self.debug:
            print(html)
        return html
    return ""

get_json(path, expected_status_code=200)

Get JSON content for the given path

Parameters:

Name Type Description Default
path str

The path to request

required
expected_status_code int

The expected HTTP status code

200

Returns:

Type Description
Any

The parsed JSON data

Source code in ngwidgets/test_base_webserver.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def get_json(self, path: str, expected_status_code: int = 200) -> Any:
    """
    Get JSON content for the given path

    Args:
        path: The path to request
        expected_status_code: The expected HTTP status code

    Returns:
        The parsed JSON data
    """
    response = self.get_response(path, expected_status_code)
    try:
        if hasattr(response, "json"):
            # Both requests.Response and TestClient responses have a json method
            return response.json()
        else:
            # Fallback
            return json.loads(response.content)
    except json.JSONDecodeError as e:
        self.fail(f"Failed to decode JSON for request {path}: {str(e)}")

get_response(path, expected_status_code=200)

Get a response for the given path and verify its status code

Parameters:

Name Type Description Default
path str

The path to request

required
expected_status_code int

The expected HTTP status code

200

Returns:

Type Description
Any

The response object

Source code in ngwidgets/test_base_webserver.py
154
155
156
157
158
159
160
161
162
163
164
165
def get_response(self, path: str, expected_status_code: int = 200) -> Any:
    """
    Get a response for the given path and verify its status code

    Args:
        path: The path to request
        expected_status_code: The expected HTTP status code

    Returns:
        The response object
    """
    raise NotImplementedError("Subclasses must implement get_response method")

setUp(debug=False, profile=True)

Set up the test environment

Source code in ngwidgets/test_base_webserver.py
150
151
152
def setUp(self, debug=False, profile=True):
    """Set up the test environment"""
    super().setUp(debug=debug, profile=profile)

ThreadedServerRunner

run the Nicegui Server in a thread

Source code in ngwidgets/test_base_webserver.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
class ThreadedServerRunner:
    """
    run the Nicegui Server in a thread
    """

    def __init__(
        self,
        ws: Any,
        args: Optional[Namespace] = None,
        shutdown_timeout: float = 5.0,
        debug: bool = False,
    ) -> None:
        """
        Initialize the ThreadedServerRunner with a web server instance, optional arguments, and a shutdown timeout.

        Args:
            ws: The web server instance to run.
            args: Optional arguments to pass to the web server's run method.
            shutdown_timeout: The maximum time in seconds to wait for the server to shutdown.
            debug: sets the debugging mode
        """
        self.debug = debug
        self.ws = ws
        self.args = args
        # notify the ws environment about the args early
        # works around configure_run not being properly called in some cases
        if args is not None:
            self.ws.args = args
        self.shutdown_timeout = shutdown_timeout
        self.thread = threading.Thread(target=self._run_server)
        self.thread.daemon = True

    def _run_server(self) -> None:
        """Internal method to run the server."""
        # prevent middleware error if app already started
        if hasattr(app, "_already_running"):
            self.ws.run = lambda _args: None  # avoid double run
        else:
            app._already_running = True  # mark NiceGUI as started
        # The run method will be called with the stored argparse.Namespace
        self.ws.run(self.args)

    def start(self) -> None:
        """Start the web server thread."""
        self.thread.start()

    def warn(self, msg: str):
        """
        show the given warning message
        """
        if self.debug:
            print(msg, file=sys.stderr)

    def stop(self) -> None:
        """
        Stop the web server thread, signaling the server to exit if it is still running.
        """
        if self.thread.is_alive():
            # Mark the start time of the shutdown
            start_time = time.time()
            # call the shutdown see https://github.com/zauberzeug/nicegui/discussions/1957
            try:
                app.shutdown()
            except Exception as e:
                if self.debug and "CancelledError" not in str(e):
                    self.warn(f"Shutdown exception: {e}")

            # Initialize the timer for timeout
            end_time = start_time + self.shutdown_timeout

            # Wait for the server to shut down, but only as long as the timeout
            while self.thread.is_alive() and time.time() < end_time:
                time.sleep(0.05)  # Sleep to prevent busy waiting

            # Calculate the total shutdown time
            shutdown_time_taken = time.time() - start_time

            if self.thread.is_alive():
                # The server didn't shut down within the timeout, handle appropriately
                if self.debug:
                    self.warn(
                        f"Warning: The server did not shut down gracefully within the timeout period. Shutdown attempt took {shutdown_time_taken:.2f} seconds."
                    )
            else:
                # If shutdown was successful, report the time taken
                if self.debug:
                    self.warn(
                        f"Server shutdown completed in {shutdown_time_taken:.2f} seconds."
                    )

    def cancel_pending_tasks(self):
        """Cancel pending async tasks to prevent loop closed errors - wait for graceful completion first"""
        if not self.thread.is_alive():
            return

        try:
            loop = getattr(self.ws, "_loop", None)
            if not loop or loop.is_closed():
                return

            pending = [t for t in asyncio.all_tasks(loop) if not t.done()]
            if not pending:
                return

            # Wait for graceful completion first
            grace_period = min(2.0, self.shutdown_timeout * 0.4)
            end_time = time.time() + grace_period

            while pending and time.time() < end_time:
                pending = [t for t in pending if not t.done()]
                time.sleep(0.1)

            # Cancel remaining tasks
            for task in pending:
                if not task.done():
                    task.cancel()

        except Exception as ex:
            self.warn(f"cancel pending tasks failed: {ex}")

__init__(ws, args=None, shutdown_timeout=5.0, debug=False)

Initialize the ThreadedServerRunner with a web server instance, optional arguments, and a shutdown timeout.

Parameters:

Name Type Description Default
ws Any

The web server instance to run.

required
args Optional[Namespace]

Optional arguments to pass to the web server's run method.

None
shutdown_timeout float

The maximum time in seconds to wait for the server to shutdown.

5.0
debug bool

sets the debugging mode

False
Source code in ngwidgets/test_base_webserver.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def __init__(
    self,
    ws: Any,
    args: Optional[Namespace] = None,
    shutdown_timeout: float = 5.0,
    debug: bool = False,
) -> None:
    """
    Initialize the ThreadedServerRunner with a web server instance, optional arguments, and a shutdown timeout.

    Args:
        ws: The web server instance to run.
        args: Optional arguments to pass to the web server's run method.
        shutdown_timeout: The maximum time in seconds to wait for the server to shutdown.
        debug: sets the debugging mode
    """
    self.debug = debug
    self.ws = ws
    self.args = args
    # notify the ws environment about the args early
    # works around configure_run not being properly called in some cases
    if args is not None:
        self.ws.args = args
    self.shutdown_timeout = shutdown_timeout
    self.thread = threading.Thread(target=self._run_server)
    self.thread.daemon = True

cancel_pending_tasks()

Cancel pending async tasks to prevent loop closed errors - wait for graceful completion first

Source code in ngwidgets/test_base_webserver.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def cancel_pending_tasks(self):
    """Cancel pending async tasks to prevent loop closed errors - wait for graceful completion first"""
    if not self.thread.is_alive():
        return

    try:
        loop = getattr(self.ws, "_loop", None)
        if not loop or loop.is_closed():
            return

        pending = [t for t in asyncio.all_tasks(loop) if not t.done()]
        if not pending:
            return

        # Wait for graceful completion first
        grace_period = min(2.0, self.shutdown_timeout * 0.4)
        end_time = time.time() + grace_period

        while pending and time.time() < end_time:
            pending = [t for t in pending if not t.done()]
            time.sleep(0.1)

        # Cancel remaining tasks
        for task in pending:
            if not task.done():
                task.cancel()

    except Exception as ex:
        self.warn(f"cancel pending tasks failed: {ex}")

start()

Start the web server thread.

Source code in ngwidgets/test_base_webserver.py
63
64
65
def start(self) -> None:
    """Start the web server thread."""
    self.thread.start()

stop()

Stop the web server thread, signaling the server to exit if it is still running.

Source code in ngwidgets/test_base_webserver.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def stop(self) -> None:
    """
    Stop the web server thread, signaling the server to exit if it is still running.
    """
    if self.thread.is_alive():
        # Mark the start time of the shutdown
        start_time = time.time()
        # call the shutdown see https://github.com/zauberzeug/nicegui/discussions/1957
        try:
            app.shutdown()
        except Exception as e:
            if self.debug and "CancelledError" not in str(e):
                self.warn(f"Shutdown exception: {e}")

        # Initialize the timer for timeout
        end_time = start_time + self.shutdown_timeout

        # Wait for the server to shut down, but only as long as the timeout
        while self.thread.is_alive() and time.time() < end_time:
            time.sleep(0.05)  # Sleep to prevent busy waiting

        # Calculate the total shutdown time
        shutdown_time_taken = time.time() - start_time

        if self.thread.is_alive():
            # The server didn't shut down within the timeout, handle appropriately
            if self.debug:
                self.warn(
                    f"Warning: The server did not shut down gracefully within the timeout period. Shutdown attempt took {shutdown_time_taken:.2f} seconds."
                )
        else:
            # If shutdown was successful, report the time taken
            if self.debug:
                self.warn(
                    f"Server shutdown completed in {shutdown_time_taken:.2f} seconds."
                )

warn(msg)

show the given warning message

Source code in ngwidgets/test_base_webserver.py
67
68
69
70
71
72
def warn(self, msg: str):
    """
    show the given warning message
    """
    if self.debug:
        print(msg, file=sys.stderr)

test_live

LiveTest Minimal live environment using Webserver/WebSolution/WebCmd of nicegui_widgets environment WF 2025-05-18

LiveCmd

Bases: WebserverCmd

Minimal CLI cmd class with only --timeout

Source code in ngwidgets/test_live.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
class LiveCmd(WebserverCmd):
    """
    Minimal CLI cmd class with only --timeout
    """

    def __init__(self, config=None, webserver_cls=None):
        if config is None:
            # Get the config from the webserver class
            config = LiveWebserver.get_config()
        if webserver_cls is None:
            webserver_cls = LiveWebserver
        # Initialize with the config and webserver class
        super().__init__(config=config, webserver_cls=webserver_cls)

    def getArgParser(self, description=None, version_msg=None):
        # Get the parser from the parent class
        parser = super().getArgParser(description, version_msg)

        # Add our timeout parameter
        parser.add_argument(
            "--timeout", type=float, default=5.0, help="Shutdown timeout"
        )
        return parser

LiveServerRunner

Runs a NiceGUI Webserver in a thread

Source code in ngwidgets/test_live.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
class LiveServerRunner:
    """
    Runs a NiceGUI Webserver in a thread
    """

    def __init__(self, ws_cmd: WebserverCmd, args: List[str], timeout=5.0):
        self.ws_cmd = ws_cmd
        self.timeout = timeout
        self.thread = threading.Thread(target=self.ws_cmd.cmd_main, args=(args,))
        self.thread.daemon = True

    def start(self):
        app._already_running = True
        self.thread.start()

    def stop(self):
        app.shutdown()
        self.thread.join(timeout=self.timeout)

LiveSolution

Bases: InputWebSolution

Minimal InputWebSolution subclass with a JSON test route

Source code in ngwidgets/test_live.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class LiveSolution(InputWebSolution):
    """
    Minimal InputWebSolution subclass with a JSON test route
    """

    def __init__(self, webserver: "LiveWebserver", client: Client):
        """
        Initialize the LiveSolution.

        Args:
            webserver (LiveWebserver): The webserver instance associated with this context.
            client (Client): The client instance this context is associated with.
        """
        super().__init__(webserver, client)

    async def livetest(self):
        """
        Test endpoint that returns HTML response
        """

        def setup_test():
            ui.label("Test endpoint is working!")

        await self.setup_content_div(setup_test)

    async def home(self):
        """
        Provide the main content page
        """

        def setup_home():
            ui.label("Welcome to the Live Nicegui Widgets test environment!")

        await self.setup_content_div(setup_home)

__init__(webserver, client)

Initialize the LiveSolution.

Parameters:

Name Type Description Default
webserver LiveWebserver

The webserver instance associated with this context.

required
client Client

The client instance this context is associated with.

required
Source code in ngwidgets/test_live.py
29
30
31
32
33
34
35
36
37
def __init__(self, webserver: "LiveWebserver", client: Client):
    """
    Initialize the LiveSolution.

    Args:
        webserver (LiveWebserver): The webserver instance associated with this context.
        client (Client): The client instance this context is associated with.
    """
    super().__init__(webserver, client)

home() async

Provide the main content page

Source code in ngwidgets/test_live.py
49
50
51
52
53
54
55
56
57
async def home(self):
    """
    Provide the main content page
    """

    def setup_home():
        ui.label("Welcome to the Live Nicegui Widgets test environment!")

    await self.setup_content_div(setup_home)

livetest() async

Test endpoint that returns HTML response

Source code in ngwidgets/test_live.py
39
40
41
42
43
44
45
46
47
async def livetest(self):
    """
    Test endpoint that returns HTML response
    """

    def setup_test():
        ui.label("Test endpoint is working!")

    await self.setup_content_div(setup_test)

LiveWebTest

Bases: BaseWebserverTest

Real IO LiveTest against LiveWebserver using HTTP requests

Source code in ngwidgets/test_live.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
class LiveWebTest(BaseWebserverTest):
    """
    Real IO LiveTest against LiveWebserver using HTTP requests
    """

    def setUp(self, debug=False, profile=True):
        """Set up the test environment"""
        super().setUp(debug=debug, profile=profile)
        self.base_url = self.__class__.base_url  # Get base_url from class variable

    @classmethod
    def setUpClass(cls):
        """Set up resources shared by all test methods"""
        # Create the webserver
        cls.ws = LiveWebserver()

        # Create the cmd instance and get properly configured args
        cls.cmd = LiveCmd()
        cls.start_runner()

    @classmethod
    def start_runner(cls):
        # Parse minimal arguments - just --serve is needed for testing
        args = ["--serve"]

        # Create runner with proper args
        cls.runner = LiveServerRunner(cls.cmd, args=args)
        cls.runner.start()
        cls.base_url = f"http://127.0.0.1:{cls.ws.config.default_port}"
        cls.max_secs_to_wait = 5
        cls.wait_until_ready(cls.max_secs_to_wait)

    @classmethod
    def tearDownClass(cls):
        """Clean up resources used by all test methods"""
        if hasattr(cls, "runner") and cls.runner:
            cls.runner.stop()

    @classmethod
    def wait_until_ready(cls, secs: int):
        """Wait until the server is ready to accept requests"""
        url = f"{cls.base_url}/live-htmltest"
        for _ in range(secs * 10):
            try:
                r = requests.get(url)
                if r.status_code == 200:
                    return
            except Exception:
                pass
            time.sleep(0.1)  # 100 millisecs per loop
        raise TimeoutError("Live server did not start")

    def get_response(self, path: str, expected_status_code: int = 200) -> Any:
        """
        Get a response for the given path using HTTP requests

        Args:
            path: The path to request
            expected_status_code: The expected HTTP status code

        Returns:
            The requests.Response object
        """
        url = f"{self.base_url}{path}"
        response = requests.get(url)
        self.assertEqual(response.status_code, expected_status_code)
        return response

    def get_html_for_post(self, path: str, data) -> str:
        """
        Get HTML content for the given path by posting the given data

        Args:
            path: The path to request
            data: The data to post (will be serialized to JSON)

        Returns:
            The HTML content as a string
        """
        url = f"{self.base_url}{path}"
        response = requests.post(url, json=data)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(response.content is not None)
        html = response.content.decode()
        if self.debug:
            print(html)
        return html

    def set_test_handler(self, handler: Callable[[], Dict[str, Any]]):
        """
        Set the handler function for the test endpoint

        Args:
            handler: Function that returns a dictionary to be converted to JSON
        """
        self.ws.set_test_handler(handler)

    def get_test_url(self):
        """
        Get the URL for the JSON API endpoint

        Returns:
            The URL for the test API
        """
        return f"{self.base_url}/live-testhandler"

get_html_for_post(path, data)

Get HTML content for the given path by posting the given data

Parameters:

Name Type Description Default
path str

The path to request

required
data

The data to post (will be serialized to JSON)

required

Returns:

Type Description
str

The HTML content as a string

Source code in ngwidgets/test_live.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def get_html_for_post(self, path: str, data) -> str:
    """
    Get HTML content for the given path by posting the given data

    Args:
        path: The path to request
        data: The data to post (will be serialized to JSON)

    Returns:
        The HTML content as a string
    """
    url = f"{self.base_url}{path}"
    response = requests.post(url, json=data)
    self.assertEqual(response.status_code, 200)
    self.assertTrue(response.content is not None)
    html = response.content.decode()
    if self.debug:
        print(html)
    return html

get_response(path, expected_status_code=200)

Get a response for the given path using HTTP requests

Parameters:

Name Type Description Default
path str

The path to request

required
expected_status_code int

The expected HTTP status code

200

Returns:

Type Description
Any

The requests.Response object

Source code in ngwidgets/test_live.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
def get_response(self, path: str, expected_status_code: int = 200) -> Any:
    """
    Get a response for the given path using HTTP requests

    Args:
        path: The path to request
        expected_status_code: The expected HTTP status code

    Returns:
        The requests.Response object
    """
    url = f"{self.base_url}{path}"
    response = requests.get(url)
    self.assertEqual(response.status_code, expected_status_code)
    return response

get_test_url()

Get the URL for the JSON API endpoint

Returns:

Type Description

The URL for the test API

Source code in ngwidgets/test_live.py
260
261
262
263
264
265
266
267
def get_test_url(self):
    """
    Get the URL for the JSON API endpoint

    Returns:
        The URL for the test API
    """
    return f"{self.base_url}/live-testhandler"

setUp(debug=False, profile=True)

Set up the test environment

Source code in ngwidgets/test_live.py
168
169
170
171
def setUp(self, debug=False, profile=True):
    """Set up the test environment"""
    super().setUp(debug=debug, profile=profile)
    self.base_url = self.__class__.base_url  # Get base_url from class variable

setUpClass() classmethod

Set up resources shared by all test methods

Source code in ngwidgets/test_live.py
173
174
175
176
177
178
179
180
181
@classmethod
def setUpClass(cls):
    """Set up resources shared by all test methods"""
    # Create the webserver
    cls.ws = LiveWebserver()

    # Create the cmd instance and get properly configured args
    cls.cmd = LiveCmd()
    cls.start_runner()

set_test_handler(handler)

Set the handler function for the test endpoint

Parameters:

Name Type Description Default
handler Callable[[], Dict[str, Any]]

Function that returns a dictionary to be converted to JSON

required
Source code in ngwidgets/test_live.py
251
252
253
254
255
256
257
258
def set_test_handler(self, handler: Callable[[], Dict[str, Any]]):
    """
    Set the handler function for the test endpoint

    Args:
        handler: Function that returns a dictionary to be converted to JSON
    """
    self.ws.set_test_handler(handler)

tearDownClass() classmethod

Clean up resources used by all test methods

Source code in ngwidgets/test_live.py
195
196
197
198
199
@classmethod
def tearDownClass(cls):
    """Clean up resources used by all test methods"""
    if hasattr(cls, "runner") and cls.runner:
        cls.runner.stop()

wait_until_ready(secs) classmethod

Wait until the server is ready to accept requests

Source code in ngwidgets/test_live.py
201
202
203
204
205
206
207
208
209
210
211
212
213
@classmethod
def wait_until_ready(cls, secs: int):
    """Wait until the server is ready to accept requests"""
    url = f"{cls.base_url}/live-htmltest"
    for _ in range(secs * 10):
        try:
            r = requests.get(url)
            if r.status_code == 200:
                return
        except Exception:
            pass
        time.sleep(0.1)  # 100 millisecs per loop
    raise TimeoutError("Live server did not start")

LiveWebserver

Bases: InputWebserver

Minimal InputWebserver setup with configurable test handler

Source code in ngwidgets/test_live.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class LiveWebserver(InputWebserver):
    """
    Minimal InputWebserver setup with configurable test handler
    """

    @classmethod
    def get_config(cls) -> WebserverConfig:
        config = WebserverConfig(
            short_name="livetest",
            default_port=8668,
            version=Version(),
        )
        config.solution_class = LiveSolution
        return config

    def __init__(self):
        """
        constructor for Webserver
        """
        super().__init__(config=self.get_config())

        # Default test handler returns a simple status message
        self._test_handler = lambda: {"status": "ok"}

        # Register the UI route for connectivity testing
        @ui.page("/live-htmltest")
        async def test_route(client: Client):
            return await self.page(client, LiveSolution.livetest)

        # Add a direct FastAPI endpoint for JSON responses
        @app.get("/live-testhandler")
        async def livetest():
            # Get the current handler from the webserver
            handler = self.get_test_handler()
            # Call the handler to get the data
            json_data = handler()
            # Return the data directly which FastAPI will serialize to JSON
            return json_data

    def set_test_handler(self, handler: Callable[[], Dict[str, Any]]):
        """
        Set the handler function for the test endpoint

        Args:
            handler: Function that returns a dictionary to be converted to JSON
        """
        self._test_handler = handler

    def get_test_handler(self) -> Callable[[], Dict[str, Any]]:
        """
        Get the current test handler

        Returns:
            The current handler function
        """
        return self._test_handler

__init__()

constructor for Webserver

Source code in ngwidgets/test_live.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def __init__(self):
    """
    constructor for Webserver
    """
    super().__init__(config=self.get_config())

    # Default test handler returns a simple status message
    self._test_handler = lambda: {"status": "ok"}

    # Register the UI route for connectivity testing
    @ui.page("/live-htmltest")
    async def test_route(client: Client):
        return await self.page(client, LiveSolution.livetest)

    # Add a direct FastAPI endpoint for JSON responses
    @app.get("/live-testhandler")
    async def livetest():
        # Get the current handler from the webserver
        handler = self.get_test_handler()
        # Call the handler to get the data
        json_data = handler()
        # Return the data directly which FastAPI will serialize to JSON
        return json_data

get_test_handler()

Get the current test handler

Returns:

Type Description
Callable[[], Dict[str, Any]]

The current handler function

Source code in ngwidgets/test_live.py
108
109
110
111
112
113
114
115
def get_test_handler(self) -> Callable[[], Dict[str, Any]]:
    """
    Get the current test handler

    Returns:
        The current handler function
    """
    return self._test_handler

set_test_handler(handler)

Set the handler function for the test endpoint

Parameters:

Name Type Description Default
handler Callable[[], Dict[str, Any]]

Function that returns a dictionary to be converted to JSON

required
Source code in ngwidgets/test_live.py
 99
100
101
102
103
104
105
106
def set_test_handler(self, handler: Callable[[], Dict[str, Any]]):
    """
    Set the handler function for the test endpoint

    Args:
        handler: Function that returns a dictionary to be converted to JSON
    """
    self._test_handler = handler

tour

Created on 2025-01-19

@author: wf

Tour definition as Legs connecting Locs Styling of Legs

Leg

A segment of a trip between two locations

Source code in ngwidgets/tour.py
31
32
33
34
35
36
37
38
39
40
@lod_storable
class Leg:
    """
    A segment of a trip between two locations
    """

    leg_type: str  # e.g., "bike", "train", "car"
    start: Loc
    end: Loc
    url: Optional[str] = None

LegStyle

Style configuration for a transport leg

Source code in ngwidgets/tour.py
81
82
83
84
85
86
87
88
89
90
91
92
93
@lod_storable
class LegStyle:
    """
    Style configuration for a transport leg
    """

    leg_type: str  # e.g. "bike", "train", "car", "ferry", "bus", "plane"
    point_type: str
    color: str
    utf8_icon: str
    weight: int
    dashArray: Optional[str]
    opacity: float

LegStyles

Collection of predefined styles for different leg types

Source code in ngwidgets/tour.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
@lod_storable
class LegStyles:
    """
    Collection of predefined styles for different leg types
    """

    styles: Dict[str, LegStyle] = field(default_factory=dict)

    def get_style(self, leg_type: str) -> LegStyle:
        """
        Get style for given leg type
        """
        leg_style = self.styles.get(leg_type)
        return leg_style

    @classmethod
    def default(cls) -> "LegStyles":
        """
        Get default leg styles
        """
        default_styles = {
            "bike": LegStyle(
                leg_type="bike",
                point_type="knooppunt",
                color="#FF0000",  # bike - red (avoid green due to map background)
                utf8_icon="🚲",
                weight=3,
                dashArray=None,
                opacity=1.0,
            ),
            "train": LegStyle(
                leg_type="train",
                point_type="train_station",
                color="#555555",  # train - dark gray (improves contrast over black)
                utf8_icon="🚂",
                weight=3,
                dashArray="10,10",
                opacity=1.0,
            ),
            "car": LegStyle(
                leg_type="car",
                point_type="parking",
                color="#404040",  # car - medium gray
                utf8_icon="🚗",
                weight=3,
                dashArray=None,
                opacity=1.0,
            ),
            "ferry": LegStyle(
                leg_type="ferry",
                point_type="ferry_terminal",
                color="#1E90FF",  # ferry - dodger blue for visibility
                utf8_icon="⛴️",
                weight=3,
                dashArray="15,10",
                opacity=0.8,
            ),
            "bus": LegStyle(
                leg_type="bus",
                point_type="bus_stop",
                color="#FF4500",  # bus - orange-red for distinctiveness
                utf8_icon="🚌",
                weight=3,
                dashArray=None,
                opacity=1.0,
            ),
            "plane": LegStyle(
                leg_type="plane",
                point_type="airport",
                color="#4B0082",  # plane - indigo for uniqueness
                utf8_icon="✈️",
                weight=3,
                dashArray="20,10,5,10",
                opacity=0.7,
            ),
        }
        return cls(styles=default_styles)

default() classmethod

Get default leg styles

Source code in ngwidgets/tour.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
@classmethod
def default(cls) -> "LegStyles":
    """
    Get default leg styles
    """
    default_styles = {
        "bike": LegStyle(
            leg_type="bike",
            point_type="knooppunt",
            color="#FF0000",  # bike - red (avoid green due to map background)
            utf8_icon="🚲",
            weight=3,
            dashArray=None,
            opacity=1.0,
        ),
        "train": LegStyle(
            leg_type="train",
            point_type="train_station",
            color="#555555",  # train - dark gray (improves contrast over black)
            utf8_icon="🚂",
            weight=3,
            dashArray="10,10",
            opacity=1.0,
        ),
        "car": LegStyle(
            leg_type="car",
            point_type="parking",
            color="#404040",  # car - medium gray
            utf8_icon="🚗",
            weight=3,
            dashArray=None,
            opacity=1.0,
        ),
        "ferry": LegStyle(
            leg_type="ferry",
            point_type="ferry_terminal",
            color="#1E90FF",  # ferry - dodger blue for visibility
            utf8_icon="⛴️",
            weight=3,
            dashArray="15,10",
            opacity=0.8,
        ),
        "bus": LegStyle(
            leg_type="bus",
            point_type="bus_stop",
            color="#FF4500",  # bus - orange-red for distinctiveness
            utf8_icon="🚌",
            weight=3,
            dashArray=None,
            opacity=1.0,
        ),
        "plane": LegStyle(
            leg_type="plane",
            point_type="airport",
            color="#4B0082",  # plane - indigo for uniqueness
            utf8_icon="✈️",
            weight=3,
            dashArray="20,10,5,10",
            opacity=0.7,
        ),
    }
    return cls(styles=default_styles)

get_style(leg_type)

Get style for given leg type

Source code in ngwidgets/tour.py
104
105
106
107
108
109
def get_style(self, leg_type: str) -> LegStyle:
    """
    Get style for given leg type
    """
    leg_style = self.styles.get(leg_type)
    return leg_style

Loc

A location in a trip or tour

Source code in ngwidgets/tour.py
17
18
19
20
21
22
23
24
25
26
27
28
@lod_storable
class Loc:
    """
    A location in a trip or tour
    """

    id: str
    coordinates: Tuple[float, float]  # latlon
    name: str
    loc_type: Optional[str] = None
    url: Optional[str] = None
    notes: Optional[str] = None

Tour

A sequence of legs connecting waypoints that form a complete journey

Source code in ngwidgets/tour.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@lod_storable
class Tour:
    """
    A sequence of legs connecting waypoints that form a complete journey
    """

    name: str
    legs: list[Leg] = field(default_factory=list)
    description: Optional[str] = None
    url: Optional[str] = None

    def dump(self, limit: int = 10, leg_styles: "LegStyles" = None):
        """
        Print a detailed dump of the tour for debugging

        Args:
           limit: Maximum number of legs to show
        """
        if leg_styles is None:
            leg_styles = LegStyles.default()
        print(f"Tour: {self.name}")
        for i, leg in enumerate(self.legs):
            if i >= limit:
                remaining = len(self.legs) - limit
                print(f"... {remaining} more legs")
                break
            leg_style = leg_styles.get_style(leg.leg_type)
            if leg_style:
                utf8_icon = leg_style.utf8_icon
            else:
                utf8_icon = "?"
            coord_start = (
                f"({leg.start.coordinates[0]:.5f}, {leg.start.coordinates[1]:.5f})"
            )
            coord_end = f"({leg.end.coordinates[0]:.5f}, {leg.end.coordinates[1]:.5f})"
            print(f" {utf8_icon} {coord_start}{coord_end}")

dump(limit=10, leg_styles=None)

Print a detailed dump of the tour for debugging

Parameters:

Name Type Description Default
limit int

Maximum number of legs to show

10
Source code in ngwidgets/tour.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def dump(self, limit: int = 10, leg_styles: "LegStyles" = None):
    """
    Print a detailed dump of the tour for debugging

    Args:
       limit: Maximum number of legs to show
    """
    if leg_styles is None:
        leg_styles = LegStyles.default()
    print(f"Tour: {self.name}")
    for i, leg in enumerate(self.legs):
        if i >= limit:
            remaining = len(self.legs) - limit
            print(f"... {remaining} more legs")
            break
        leg_style = leg_styles.get_style(leg.leg_type)
        if leg_style:
            utf8_icon = leg_style.utf8_icon
        else:
            utf8_icon = "?"
        coord_start = (
            f"({leg.start.coordinates[0]:.5f}, {leg.start.coordinates[1]:.5f})"
        )
        coord_end = f"({leg.end.coordinates[0]:.5f}, {leg.end.coordinates[1]:.5f})"
        print(f" {utf8_icon} {coord_start}{coord_end}")

tristate

tristate.py

Created on 2023-12-10

@author: WF @author: OpenAI Assistant (ChatGPT Version 4)

This module provides a Tristate class for use in NiceGUI applications, creating a tri-state toggle input that cycles through predefined icon sets.

The implementation is inspired by examples and discussions from: - NiceGUI Custom Vue Component example: https://github.com/zauberzeug/nicegui/tree/main/examples/custom_vue_component - JSFiddle by WF: https://jsfiddle.net/wf_bitplan_com/941std72/8/ - Stack Overflow answer by WF: https://stackoverflow.com/a/27617418/1497139

Prompts for Reproducing this Code: 1. "Create a Python class Tristate in a module tristate for a NiceGUI component that serves as a tri-state toggle input." 2. "Implement icon cycling within the Python class to handle state changes without relying on JavaScript logic." 3. "Utilize Unicode icon sets within the Python class for the visual representation of each state." 4. "Ensure the Python class handles the reactivity and updates the Vue component's props when the state changes." 5. "Apply google style doc-strings and type hints for better code clarity and leverage black and isort for code formatting and import sorting." 6. "Provide comprehensive documentation within the Python class to explain the functionality and usage of each method and property." 7. "add proper authorship and iso-date of creation information in the module header" 8. "add a prompts for reproducing this code in the header comment section that will allow any proper LLM to reproduce this code" 9. "Introduce an update method invocation within the state change logic to trigger a re-render of the component in NiceGUI. 10. "add links to https://github.com/zauberzeug/nicegui/tree/main/examples/custom_vue_component, https://jsfiddle.net/wf_bitplan_com/941std72/8/ and https://stackoverflow.com/a/27617418/1497139 for proper reference" 11. "Include the following Unicode icon sets in the Tristate component for NiceGUI: 'arrows' with Left Arrow ('←'), Up-Down Arrow ('↕️'), and Right Arrow ('→'); 'ballot' with Ballot Box ('☐'), Ballot Box with Check ('☑️'), and Ballot Box with X ('☒️'); 'check' with Checkbox ('☐'), Question Mark ('❔'), and Checkmark ('✔️'); 'circles' with Circle ('⭘'), Bullseye ('🎯'), and Fisheye ('🔘'); 'electrical' with Plug ('🔌'), Battery Half ('🔋'), and Lightning ('⚡'); 'faces' with Sad Face ('☹️'), Neutral Face ('😐'), and Happy Face ('☺️'); 'hands' with Thumbs Down ('👎'), Hand ('✋'), and Thumbs Up ('👍'); 'hearts' with Empty Heart ('♡'), Half Heart ('❤️'), and Full Heart ('❤️'); 'locks' with Unlocked ('🔓'), Locked with Pen ('🔏'), and Locked ('🔒'); 'marks' with Question Mark ('❓'), Check Mark ('✅'), and Cross Mark ('❌'); 'moons' with New Moon ('🌑'), Half Moon ('🌓'), and Full Moon ('🌕'); 'musical_notes' with Single Note ('♪'), Double Note ('♫'), and Multiple Notes ('🎶'); 'stars' with Empty Star ('☆'), Half Star ('★'), and Full Star ('★'); 'traffic_lights' with Red ('🔴'), Yellow ('🟡'), and Green ('🟢'); 'weather' with Cloud ('☁️'), Sun ('☀️'), and Thunderstorm ('⛈️')."

Tristate

Bases: Element

A Tristate toggle component for NiceGUI.

Attributes:

Name Type Description
icon_set str

The name of the icon set to use.

current_icon_index int

The index of the current icon in the set.

style str

CSS styling for the input element.

Source code in ngwidgets/tristate.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
class Tristate(Element, component="tristate.js"):
    """
    A Tristate toggle component for NiceGUI.

    Attributes:
        icon_set (str): The name of the icon set to use.
        current_icon_index (int): The index of the current icon in the set.
        style (str): CSS styling for the input element.
    """

    ICON_SETS = {
        "arrows": ["←", "↕️", "→"],  # Left Arrow, Up-Down Arrow, Right Arrow
        "ballot": [
            "☐",
            "☑️",
            "☒️",
        ],  # Ballot Box, Ballot Box with Check, Ballot Box with X
        "check": ["☐", "❔", "✔️"],  # Checkbox, Question Mark, Checkmark
        "circles": ["⭘", "🎯", "🔘"],  # Circle, Bullseye, Fisheye
        "electrical": ["🔌", "🔋", "⚡"],  # Plug, Battery Half, Lightning
        "faces": ["☹️", "😐", "☺️"],  # Sad Face, Neutral Face, Happy Face
        "hands": ["👎", "✋", "👍"],  # Thumbs Down, Hand, Thumbs Up
        "hearts": ["♡", "❤️", "❤️"],  # Empty Heart, Half Heart, Full Heart
        "locks": ["🔓", "🔏", "🔒"],  # Unlocked, Locked with Pen, Locked
        "marks": ["❓", "✅", "❌"],  # Question, Check, Cross
        "moons": ["🌑", "🌓", "🌕"],  # New Moon, Half Moon, Full Moon
        "musical_notes": ["♪", "♫", "🎶"],  # Single Note, Double Note, Multiple Notes
        "stars": ["☆", "★", "★"],  # Empty Star, Half Star, Full Star
        "traffic_lights": ["🔴", "🟡", "🟢"],  # Red, Yellow, Green
        "weather": ["☁️", "☀️", "⛈️"],  # Cloud, Sun, Thunderstorm
    }

    def __init__(
        self,
        icon_set_name: str = "marks",
        style: str = "border: none;",
        on_change: Optional[Callable] = None,
    ) -> None:
        """
        Initializes the Tristate component.

        Args:
            icon_set_name (str): The name of the icon set to use. Default is 'marks'.
            style (str): CSS styling for the input element. Default is 'border: none;'.
            on_change (Optional[Callable]): Callback function for state changes.
        """
        super().__init__()
        self.icon_set_name = icon_set_name
        self.icon_set = Tristate.ICON_SETS[icon_set_name]
        self.current_icon_index = 0
        self.style(style)
        self.user_on_change = on_change
        self.on("change", self.on_change)

        self.update_props()

    def on_change(self, _=None) -> None:
        """Handles the change event, cycles the icon, and calls user-defined callback if any."""
        self.cycle_icon()
        if self.user_on_change is not None:
            self.user_on_change()

    def cycle_icon(self, _=None) -> None:
        """Cycles through the icons in the set and updates the component."""
        self.current_icon_index = (self.current_icon_index + 1) % len(self.icon_set)
        self.update_props()

    def update_props(self) -> None:
        """Updates the component properties with the current icon and style."""
        self.utf8_icon = self.icon_set[self.current_icon_index]
        self._props["value"] = self.utf8_icon
        self._props["style"] = self.style
        self.update()

__init__(icon_set_name='marks', style='border: none;', on_change=None)

Initializes the Tristate component.

Parameters:

Name Type Description Default
icon_set_name str

The name of the icon set to use. Default is 'marks'.

'marks'
style str

CSS styling for the input element. Default is 'border: none;'.

'border: none;'
on_change Optional[Callable]

Callback function for state changes.

None
Source code in ngwidgets/tristate.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def __init__(
    self,
    icon_set_name: str = "marks",
    style: str = "border: none;",
    on_change: Optional[Callable] = None,
) -> None:
    """
    Initializes the Tristate component.

    Args:
        icon_set_name (str): The name of the icon set to use. Default is 'marks'.
        style (str): CSS styling for the input element. Default is 'border: none;'.
        on_change (Optional[Callable]): Callback function for state changes.
    """
    super().__init__()
    self.icon_set_name = icon_set_name
    self.icon_set = Tristate.ICON_SETS[icon_set_name]
    self.current_icon_index = 0
    self.style(style)
    self.user_on_change = on_change
    self.on("change", self.on_change)

    self.update_props()

cycle_icon(_=None)

Cycles through the icons in the set and updates the component.

Source code in ngwidgets/tristate.py
118
119
120
121
def cycle_icon(self, _=None) -> None:
    """Cycles through the icons in the set and updates the component."""
    self.current_icon_index = (self.current_icon_index + 1) % len(self.icon_set)
    self.update_props()

on_change(_=None)

Handles the change event, cycles the icon, and calls user-defined callback if any.

Source code in ngwidgets/tristate.py
112
113
114
115
116
def on_change(self, _=None) -> None:
    """Handles the change event, cycles the icon, and calls user-defined callback if any."""
    self.cycle_icon()
    if self.user_on_change is not None:
        self.user_on_change()

update_props()

Updates the component properties with the current icon and style.

Source code in ngwidgets/tristate.py
123
124
125
126
127
128
def update_props(self) -> None:
    """Updates the component properties with the current icon and style."""
    self.utf8_icon = self.icon_set[self.current_icon_index]
    self._props["value"] = self.utf8_icon
    self._props["style"] = self.style
    self.update()

users

Created on 2023-08-15

@author: wf

Users

A class to manage user credentials stored in a JSON file.

Attributes:

Name Type Description
dir_path str

The directory path where the JSON file resides.

file_path str

The full path to the JSON file.

Source code in ngwidgets/users.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class Users:
    """
    A class to manage user credentials stored in a JSON file.

    Attributes:
        dir_path (str): The directory path where the JSON file resides.
        file_path (str): The full path to the JSON file.
    """

    def __init__(self, path_str: str):
        """
        Initialize the Users class and set file paths.
        """
        self.dir_path = os.path.expanduser(path_str)
        self.file_path = os.path.join(self.dir_path, "users.json")
        self._ensure_directory_exists()

    def _ensure_directory_exists(self):
        """Create the directory if it doesn't exist."""
        if not os.path.exists(self.dir_path):
            os.makedirs(self.dir_path)

    def save_password_data(self, data):
        """
        Save user data to the JSON file.

        Args:
            data (dict): Dictionary containing username: password pairs.
        """
        with open(self.file_path, "w") as file:
            json.dump(data, file, indent=4)

    def load_password_data(self):
        """
        Load user data from the JSON file.

        Returns:
            dict: Dictionary containing username: password pairs.
        """
        if os.path.exists(self.file_path):
            with open(self.file_path, "r") as file:
                return json.load(file)
        return {}

    def add_user(self, username, password):
        """
        Add a new user with a hashed password to the data file.

        Args:
            username (str): The username of the new user.
            password (str): The password for the new user.
        """
        hashed_password = bcrypt.hashpw(
            password.encode("utf-8"), bcrypt.gensalt()
        ).decode("utf-8")
        data = self.load_password_data()
        data[username] = hashed_password
        self.save_password_data(data)

    def check_password(self, username, password):
        """
        Validate a password against its hashed version.

        Args:
            username (str): The username of the user.
            password (str): The password to check.

        Returns:
            bool: True if the password matches, False otherwise.
        """
        data = self.load_password_data()
        hashed_password = data.get(username)
        if not hashed_password:
            return False
        ok = bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8"))
        return ok

__init__(path_str)

Initialize the Users class and set file paths.

Source code in ngwidgets/users.py
23
24
25
26
27
28
29
def __init__(self, path_str: str):
    """
    Initialize the Users class and set file paths.
    """
    self.dir_path = os.path.expanduser(path_str)
    self.file_path = os.path.join(self.dir_path, "users.json")
    self._ensure_directory_exists()

add_user(username, password)

Add a new user with a hashed password to the data file.

Parameters:

Name Type Description Default
username str

The username of the new user.

required
password str

The password for the new user.

required
Source code in ngwidgets/users.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def add_user(self, username, password):
    """
    Add a new user with a hashed password to the data file.

    Args:
        username (str): The username of the new user.
        password (str): The password for the new user.
    """
    hashed_password = bcrypt.hashpw(
        password.encode("utf-8"), bcrypt.gensalt()
    ).decode("utf-8")
    data = self.load_password_data()
    data[username] = hashed_password
    self.save_password_data(data)

check_password(username, password)

Validate a password against its hashed version.

Parameters:

Name Type Description Default
username str

The username of the user.

required
password str

The password to check.

required

Returns:

Name Type Description
bool

True if the password matches, False otherwise.

Source code in ngwidgets/users.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def check_password(self, username, password):
    """
    Validate a password against its hashed version.

    Args:
        username (str): The username of the user.
        password (str): The password to check.

    Returns:
        bool: True if the password matches, False otherwise.
    """
    data = self.load_password_data()
    hashed_password = data.get(username)
    if not hashed_password:
        return False
    ok = bcrypt.checkpw(password.encode("utf-8"), hashed_password.encode("utf-8"))
    return ok

load_password_data()

Load user data from the JSON file.

Returns:

Name Type Description
dict

Dictionary containing username: password pairs.

Source code in ngwidgets/users.py
46
47
48
49
50
51
52
53
54
55
56
def load_password_data(self):
    """
    Load user data from the JSON file.

    Returns:
        dict: Dictionary containing username: password pairs.
    """
    if os.path.exists(self.file_path):
        with open(self.file_path, "r") as file:
            return json.load(file)
    return {}

save_password_data(data)

Save user data to the JSON file.

Parameters:

Name Type Description Default
data dict

Dictionary containing username: password pairs.

required
Source code in ngwidgets/users.py
36
37
38
39
40
41
42
43
44
def save_password_data(self, data):
    """
    Save user data to the JSON file.

    Args:
        data (dict): Dictionary containing username: password pairs.
    """
    with open(self.file_path, "w") as file:
        json.dump(data, file, indent=4)

version

Created on 2023-09-12

@author: wf

Version

Version handling for nicegui widgets

Source code in ngwidgets/version.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@lod_storable
class Version:
    """
    Version handling for nicegui widgets
    """

    name = "ngwidgets"
    version = ngwidgets.__version__
    date = "2023-09-10"
    updated = "2026-08-25"
    description = "NiceGUI widgets"

    authors = "Wolfgang Fahl"

    doc_url = "https://wiki.bitplan.com/index.php/Nicegui_widgets"
    chat_url = "https://github.com/WolfgangFahl/nicegui_widgets/discussions"
    cm_url = "https://github.com/WolfgangFahl/nicegui_widgets"

    license = f"""Copyright 2023-2026 contributors. All rights reserved.

  Licensed under the Apache License 2.0
  http://www.apache.org/licenses/LICENSE-2.0

  Distributed on an "AS IS" basis without warranties
  or conditions of any kind, either express or implied."""

    longDescription = f"""{name} version {version}
{description}

  Created by {authors} on {date} last updated {updated}"""

webserver

Created on 2023-09-10

@author: wf

NiceGuiWebserver

Bases: object

a basic NiceGuiWebserver

Source code in ngwidgets/webserver.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
class NiceGuiWebserver(object):
    """
    a basic NiceGuiWebserver
    """

    def __init__(self, config: WebserverConfig = None):
        """
        Constructor
        """
        self.debug = False
        self.logger = logging.getLogger()
        self.do_trace = True
        if config is None:
            config = WebserverConfig()
        self.config = config
        self.app = core.app

    async def page(self, client: Client, wanted_action: Callable, *args, **kwargs):
        """
        Handle a page request for a specific client. This method ensures that a specific type of WebSolution
        (or its subclass) is created for each client and used throughout the client's interaction.

        Args:
            client (Client): The client making the request.
            wanted_action(Callable): The function of the solution to perform. Might be overriden so we check the solution_instance
            *args, **kwargs: Additional arguments to pass to the action.

        Returns:
            The result of the action performed.
        """
        solution_class = self.config.solution_class
        if not solution_class:
            raise TypeError("no solution_class configured")

        action_result = await self.execute_action(
            client, solution_class, wanted_action, *args, **kwargs
        )
        return action_result

    async def execute_action(
        self, client: Client, solution_class, wanted_action: Callable, *args, **kwargs
    ):
        """
        Execute the specified action on the solution instance.

        Args:
            client (Client): The client making the request.
            solution_class: The solution class to create an instance for to execute the action on.
            wanted_action(Callable): The function to perform.
            args, *kwargs: Additional arguments to pass to the action.

        Returns:
            The result of the action performed.
        """
        solution_instance = solution_class(self, client)

        # Check if the solution_instance is an instance of solution_class or its subclass
        if not isinstance(solution_instance, solution_class):
            raise TypeError(
                f"solution_instance must be an instance of {solution_class.__name__} or its subclass, not {type(solution_instance).__name__}."
            )

        # Check if the action_callable is a method of solution_instance
        if not callable(wanted_action) or not hasattr(
            solution_instance, wanted_action.__name__
        ):
            raise AttributeError(
                f"The provided callable {wanted_action.__qualname__} is not a method of {solution_instance.__class__.__name__}."
            )
        # replace action by the one from the instance for inheritance handling
        action = getattr(solution_instance, wanted_action.__name__)

        await solution_instance.prepare()

        # call any preparation code needed before the actual nicegui.ui calls
        # are done
        solution_instance.prepare_ui()

        action_result = await action(*args, **kwargs)
        return action_result

    @classmethod
    def optionalDebug(self, args):
        """
        start the remote debugger if the arguments specify so

        Args:
            args(): The command line arguments
        """
        if args.debugServer:
            import pydevd
            from pydevd_file_utils import setup_client_server_paths

            print(
                f"remotePath: {args.debugRemotePath} localPath:{args.debugLocalPath}",
                flush=True,
            )
            if args.debugRemotePath and args.debugLocalPath:
                MY_PATHS_FROM_ECLIPSE_TO_PYTHON = [
                    (args.debugRemotePath, args.debugLocalPath),
                ]
                setup_client_server_paths(MY_PATHS_FROM_ECLIPSE_TO_PYTHON)
                # os.environ["PATHS_FROM_ECLIPSE_TO_PYTHON"]='[["%s", "%s"]]' % (remotePath,localPath)
                # print("trying to debug with PATHS_FROM_ECLIPSE_TO_PYTHON=%s" % os.environ["PATHS_FROM_ECLIPSE_TO_PYTHON"]);

            pydevd.settrace(
                args.debugServer,
                port=args.debugPort,
                stdoutToServer=True,
                stderrToServer=True,
            )
            print(f"command line args are: {str(sys.argv)}")

    def run(self, args):
        """
        Runs the UI of the web server.

        Args:
            args (list): The command line arguments.
        """
        self.args = args
        self.debug = args.debug
        self.optionalDebug(args)
        # allow app specific configuration steps
        self.configure_run()
        ui.run(
            title=self.config.version.name,
            host=args.host,
            port=args.port,
            show=args.client,
            fastapi_docs=self.config.fastapi_docs,
            reload=False,
            storage_secret=self.config.storage_secret,
        )

    def configure_run(self):
        """
        Configures specific before run steps of a web server.
        This method is intended to be overridden
        by subclasses to provide custom run behavior.
        The base method does nothing and can be extended in subclasses.
        """
        pass

    def stop(self):
        """
        stop the server
        """

__init__(config=None)

Constructor

Source code in ngwidgets/webserver.py
116
117
118
119
120
121
122
123
124
125
126
def __init__(self, config: WebserverConfig = None):
    """
    Constructor
    """
    self.debug = False
    self.logger = logging.getLogger()
    self.do_trace = True
    if config is None:
        config = WebserverConfig()
    self.config = config
    self.app = core.app

configure_run()

Configures specific before run steps of a web server. This method is intended to be overridden by subclasses to provide custom run behavior. The base method does nothing and can be extended in subclasses.

Source code in ngwidgets/webserver.py
246
247
248
249
250
251
252
253
def configure_run(self):
    """
    Configures specific before run steps of a web server.
    This method is intended to be overridden
    by subclasses to provide custom run behavior.
    The base method does nothing and can be extended in subclasses.
    """
    pass

execute_action(client, solution_class, wanted_action, *args, **kwargs) async

Execute the specified action on the solution instance.

Parameters:

Name Type Description Default
client Client

The client making the request.

required
solution_class

The solution class to create an instance for to execute the action on.

required
wanted_action Callable

The function to perform.

required
args, *kwargs

Additional arguments to pass to the action.

required

Returns:

Type Description

The result of the action performed.

Source code in ngwidgets/webserver.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
async def execute_action(
    self, client: Client, solution_class, wanted_action: Callable, *args, **kwargs
):
    """
    Execute the specified action on the solution instance.

    Args:
        client (Client): The client making the request.
        solution_class: The solution class to create an instance for to execute the action on.
        wanted_action(Callable): The function to perform.
        args, *kwargs: Additional arguments to pass to the action.

    Returns:
        The result of the action performed.
    """
    solution_instance = solution_class(self, client)

    # Check if the solution_instance is an instance of solution_class or its subclass
    if not isinstance(solution_instance, solution_class):
        raise TypeError(
            f"solution_instance must be an instance of {solution_class.__name__} or its subclass, not {type(solution_instance).__name__}."
        )

    # Check if the action_callable is a method of solution_instance
    if not callable(wanted_action) or not hasattr(
        solution_instance, wanted_action.__name__
    ):
        raise AttributeError(
            f"The provided callable {wanted_action.__qualname__} is not a method of {solution_instance.__class__.__name__}."
        )
    # replace action by the one from the instance for inheritance handling
    action = getattr(solution_instance, wanted_action.__name__)

    await solution_instance.prepare()

    # call any preparation code needed before the actual nicegui.ui calls
    # are done
    solution_instance.prepare_ui()

    action_result = await action(*args, **kwargs)
    return action_result

optionalDebug(args) classmethod

start the remote debugger if the arguments specify so

Parameters:

Name Type Description Default
args

The command line arguments

required
Source code in ngwidgets/webserver.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
@classmethod
def optionalDebug(self, args):
    """
    start the remote debugger if the arguments specify so

    Args:
        args(): The command line arguments
    """
    if args.debugServer:
        import pydevd
        from pydevd_file_utils import setup_client_server_paths

        print(
            f"remotePath: {args.debugRemotePath} localPath:{args.debugLocalPath}",
            flush=True,
        )
        if args.debugRemotePath and args.debugLocalPath:
            MY_PATHS_FROM_ECLIPSE_TO_PYTHON = [
                (args.debugRemotePath, args.debugLocalPath),
            ]
            setup_client_server_paths(MY_PATHS_FROM_ECLIPSE_TO_PYTHON)
            # os.environ["PATHS_FROM_ECLIPSE_TO_PYTHON"]='[["%s", "%s"]]' % (remotePath,localPath)
            # print("trying to debug with PATHS_FROM_ECLIPSE_TO_PYTHON=%s" % os.environ["PATHS_FROM_ECLIPSE_TO_PYTHON"]);

        pydevd.settrace(
            args.debugServer,
            port=args.debugPort,
            stdoutToServer=True,
            stderrToServer=True,
        )
        print(f"command line args are: {str(sys.argv)}")

page(client, wanted_action, *args, **kwargs) async

Handle a page request for a specific client. This method ensures that a specific type of WebSolution (or its subclass) is created for each client and used throughout the client's interaction.

Parameters:

Name Type Description Default
client Client

The client making the request.

required
wanted_action Callable

The function of the solution to perform. Might be overriden so we check the solution_instance

required
*args, **kwargs

Additional arguments to pass to the action.

required

Returns:

Type Description

The result of the action performed.

Source code in ngwidgets/webserver.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
async def page(self, client: Client, wanted_action: Callable, *args, **kwargs):
    """
    Handle a page request for a specific client. This method ensures that a specific type of WebSolution
    (or its subclass) is created for each client and used throughout the client's interaction.

    Args:
        client (Client): The client making the request.
        wanted_action(Callable): The function of the solution to perform. Might be overriden so we check the solution_instance
        *args, **kwargs: Additional arguments to pass to the action.

    Returns:
        The result of the action performed.
    """
    solution_class = self.config.solution_class
    if not solution_class:
        raise TypeError("no solution_class configured")

    action_result = await self.execute_action(
        client, solution_class, wanted_action, *args, **kwargs
    )
    return action_result

run(args)

Runs the UI of the web server.

Parameters:

Name Type Description Default
args list

The command line arguments.

required
Source code in ngwidgets/webserver.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def run(self, args):
    """
    Runs the UI of the web server.

    Args:
        args (list): The command line arguments.
    """
    self.args = args
    self.debug = args.debug
    self.optionalDebug(args)
    # allow app specific configuration steps
    self.configure_run()
    ui.run(
        title=self.config.version.name,
        host=args.host,
        port=args.port,
        show=args.client,
        fastapi_docs=self.config.fastapi_docs,
        reload=False,
        storage_secret=self.config.storage_secret,
    )

stop()

stop the server

Source code in ngwidgets/webserver.py
255
256
257
258
def stop(self):
    """
    stop the server
    """

WebSolution

the user/client specific web context of a solution

Source code in ngwidgets/webserver.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
class WebSolution:
    """
    the user/client specific web context of a solution
    """

    def __init__(self, webserver: NiceGuiWebserver, client: Client):
        """
        construct a client specific WebSolution
        """
        self.webserver = webserver
        self.logger = webserver.logger
        self.config = self.webserver.config
        self.args = self.webserver.args
        self.client = client
        self.log_view = None
        self.container = None

    def notify(self, msg: str):
        """
        call ui.notify with a context
        """
        with self.content_div:
            ui.notify(msg)

    async def prepare(self):
        """
        make sure this solution context is ready for use
        """
        timeout = self.config.timeout
        if timeout is not None:
            await self.client.connected(timeout=timeout)

    def prepare_ui(self):
        """
        call any code necessary before the first nicegui.ui call is
        done e.g. handling command line arguments

        The base method does nothing and serves as a placeholder for subclasses to define their own UI preparation logic.
        """
        pass

    def link_button(self, name: str, target: str, icon_name: str, new_tab: bool = True):
        """
        Creates a button with a specified icon that opens a target URL upon being clicked.

        Args:
            name (str): The name to be displayed on the button.
            target (str): The target URL that should be opened when the button is clicked.
            icon_name (str): The name of the icon to be displayed on the button.
            new_tab(bool): if True open link in new tab

        Returns:
            The button object.


        see https://fonts.google.com/icons?icon.set=Material+Icons for potential icon names
        """
        with ui.link(text=" ", target=target, new_tab=new_tab) as link_btn:
            link_btn.button = ui.button(name, icon=icon_name)
        return link_btn

    def tool_button(
        self, tooltip: str, icon: str, handler: callable = None, toggle_icon: str = None
    ) -> ui.button:
        """
        Creates a button with icon that triggers a specified function upon being clicked.

        Args:
            tooltip (str): The tooltip to be displayed.
            icon (str): The name of the icon to be displayed on the button.
            handler (function): The function to be called when the button is clicked.
            toggle_icon (str): The name of an alternative icon to be displayed when the button is clicked.

        Returns:
            ui.button: The icon button object.

        valid icons may be found at:
            https://fonts.google.com/icons
        """
        icon_button = (
            ui.button("", icon=icon, color="primary")
            .tooltip(tooltip)
            .on("click", handler=handler)
        )
        icon_button.toggle_icon = toggle_icon
        return icon_button

    def run_busy(
        self,
        func: callable,
        status: ui.label = None,
        button: ui.button = None,
        spinner: ui.spinner = None,
        on_result: callable = None,
        busy_text: str = "working ...",
        done_text: str = "done",
        timeout: float = 20.0,
    ):
        """
        run the given blocking function in the background while the user
        interface shows that it is busy - see
        https://github.com/WolfgangFahl/nicegui_widgets/issues/100

        the busy state is shown at once, the work runs off the event loop
        and every user interface update happens inside my container

        Args:
            func: the blocking function to run
            status: the label to show the state in
            button: the button to disable while busy
            spinner: the spinner to show while busy
            on_result: called with the return value of func
            busy_text: the status text while busy
            done_text: the status text when finished
            timeout: the seconds after which the task is cancelled
        """
        runner = TaskRunner(timeout=timeout)

        def set_busy(busy: bool, text: str):
            with self.container:
                if button:
                    if busy:
                        button.disable()
                    else:
                        button.enable()
                if spinner:
                    spinner.set_visibility(busy)
                if status:
                    status.set_text(text)

        def handle_result(result):
            set_busy(False, done_text)
            if on_result:
                with self.container:
                    on_result(result)

        set_busy(True, busy_text)
        runner.run_blocking(func, on_result=handle_result)
        return runner

    def toggle_icon(self, button: ui.button):
        """
        toggle the icon of the given button

        Args:
            ui.button: the button that needs the icon to be toggled
        """
        if hasattr(button, "toggle_icon"):
            # exchange icon with toggle icon
            toggle_icon = button._props["icon"]
            icon = button.toggle_icon
            button._props["icon"] = icon
            button.toggle_icon = toggle_icon
        button.update()

    def round_label(
        self, title: str, background_color: str = "#e6e6e6", **kwargs
    ) -> ui.label:
        """
        Creates a label with rounded corners and optional background color.

        Args:
            title (str): The text to be displayed in the label.
            background_color (str): Hex color code for the label's background.
                                    Defaults to a light grey color ("#e6e6e6").
            **kwargs: Additional keyword arguments passed to the select widget creation.

        Returns:
            ui.label: A NiceGUI label element with rounded corners and the specified background color.
        """
        background_style = (
            f"background-color: {background_color};" if background_color else ""
        )
        round_label = (
            ui.label(title, **kwargs)
            .classes("rounded p-2")
            .style(f"margin-right: 10px; {background_style}")
        )
        return round_label

    def add_select(
        self,
        title: str,
        selection: Union[List[Any], Dict[str, Any]],
        background_color: str = "#e6e6e6",
        **kwargs,
    ) -> Any:
        """
        Add a select widget with a given title, selection options, and optional styling.

        Args:
            title (str): The title or label for the select widget.
            selection (Union[List[Any], Dict[str, Any]]): The options available for selection.
                - If a List, each element represents an option.
                - If a Dict, keys are option labels and values are the corresponding values.
            background_color (str, optional): Hex color code for the background of the label. Defaults to "#e6e6e6".
            **kwargs: Additional keyword arguments passed to the select widget creation.

        Returns:
            Any: The created nicegui ui.select widget.
        """
        with ui.element("div").style("display: flex; align-items: center;"):
            self.round_label(title, background_color)
            s = ui.select(selection, **kwargs)
            # https://github.com/WolfgangFahl/nicegui_widgets/issues/64
            # s.validation={}
            return s

    def do_read_input(self, input_str: str) -> str:
        """Reads the given input.

        Args:
            input_str (str): The input string representing a URL or local path.

        Returns:
            str: the input content as a string
        """
        if input_str.startswith("http://") or input_str.startswith("https://"):
            with urllib.request.urlopen(input_str) as response:
                text = response.read().decode("utf-8")
                return text
        else:
            if os.path.exists(input_str):
                with open(input_str, "r") as file:
                    return file.read()
            else:
                raise Exception(f"File does not exist: {input_str}")

    async def toogle_hamburger(self):
        """
        toggle the hamburger menu
        """
        if not self.header:
            return

        self.header.toggle()
        self.footer.toggle()
        self.hamburger_button1.visible = not self.hamburger_button1.visible

    def setup_menu(self, detailed: bool = None):
        """
        set up the default menu home/settings and about

        Args:
            detailed(bool): if True add github,chat and help links
        """
        self.header = None
        version = self.config.version
        if detailed is None:
            detailed = self.config.detailed_menu
        self.config.color_schema.apply()
        # parent element for hamburger menu
        with ui.element("div").classes(
            "top-0 left-0 z-50 bg-transparent"
        ) as self.hamburger_container:
            self.hamburger_button1 = ui.button(
                icon="menu", on_click=self.toogle_hamburger
            )
            self.hamburger_button1.visible = False
        with ui.header() as self.header:
            self.hamburger_button = ui.button(
                icon="menu", on_click=self.toogle_hamburger
            )
            self.link_button("home", "/", "home", new_tab=False)
            self.link_button("settings", "/settings", "settings", new_tab=False)
            self.configure_menu()
            if detailed:
                self.link_button("github", version.cm_url, "bug_report")
                self.link_button("chat", version.chat_url, "chat")
                self.link_button("help", version.doc_url, "help")
                self.link_button("about", "/about", "info", new_tab=False)

    async def setup_footer(self):
        """
        setup the footer
        """
        with ui.footer() as self.footer:
            ui.label(self.config.copy_right)
            ui.link("Powered by nicegui", "https://nicegui.io/").style("color: #fff")

    async def setup_content_div(
        self,
        setup_content: Optional[Callable] = None,
        with_exception_handling: bool = True,
        **kwargs,
    ):
        """
        Sets up the content frame div of the web server's user interface.

        Args:
            setup_content (Optional[Callable]): A callable for setting up the main content.
                                                 It can be a regular function or a coroutine.
            with_exception_handling(bool): if True handle exceptions

        Note:
            This method is asynchronous and should be awaited when called.
        """
        # Setting up the menu
        self.setup_menu()

        with ui.element("div").classes("w-full h-full") as self.content_div:
            self.container = self.content_div
            # Execute setup_content if provided
            if setup_content:
                try:
                    if asyncio.iscoroutinefunction(setup_content):
                        await setup_content(**kwargs)
                    else:
                        setup_content(**kwargs)
                except Exception as ex:
                    if with_exception_handling:
                        self.handle_exception(ex)
                    else:
                        raise ex

        await self.setup_footer()

    def handle_exception(self, e: BaseException, trace: Optional[bool] = None):
        """Handles an exception by creating an error message.

        Args:
            e (BaseException): The exception to handle.
            trace (bool, optional): Whether to include the traceback in the error message. Default is False.
        """
        if trace is None and self.webserver:
            trace = self.webserver.do_trace
        if trace:
            self.error_msg = str(e) + "\n" + traceback.format_exc()
        else:
            self.error_msg = str(e)
        if self.log_view:
            self.log_view.push(self.error_msg)
        print(self.error_msg, file=sys.stderr)

__init__(webserver, client)

construct a client specific WebSolution

Source code in ngwidgets/webserver.py
266
267
268
269
270
271
272
273
274
275
276
def __init__(self, webserver: NiceGuiWebserver, client: Client):
    """
    construct a client specific WebSolution
    """
    self.webserver = webserver
    self.logger = webserver.logger
    self.config = self.webserver.config
    self.args = self.webserver.args
    self.client = client
    self.log_view = None
    self.container = None

add_select(title, selection, background_color='#e6e6e6', **kwargs)

Add a select widget with a given title, selection options, and optional styling.

Parameters:

Name Type Description Default
title str

The title or label for the select widget.

required
selection Union[List[Any], Dict[str, Any]]

The options available for selection. - If a List, each element represents an option. - If a Dict, keys are option labels and values are the corresponding values.

required
background_color str

Hex color code for the background of the label. Defaults to "#e6e6e6".

'#e6e6e6'
**kwargs

Additional keyword arguments passed to the select widget creation.

{}

Returns:

Name Type Description
Any Any

The created nicegui ui.select widget.

Source code in ngwidgets/webserver.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def add_select(
    self,
    title: str,
    selection: Union[List[Any], Dict[str, Any]],
    background_color: str = "#e6e6e6",
    **kwargs,
) -> Any:
    """
    Add a select widget with a given title, selection options, and optional styling.

    Args:
        title (str): The title or label for the select widget.
        selection (Union[List[Any], Dict[str, Any]]): The options available for selection.
            - If a List, each element represents an option.
            - If a Dict, keys are option labels and values are the corresponding values.
        background_color (str, optional): Hex color code for the background of the label. Defaults to "#e6e6e6".
        **kwargs: Additional keyword arguments passed to the select widget creation.

    Returns:
        Any: The created nicegui ui.select widget.
    """
    with ui.element("div").style("display: flex; align-items: center;"):
        self.round_label(title, background_color)
        s = ui.select(selection, **kwargs)
        # https://github.com/WolfgangFahl/nicegui_widgets/issues/64
        # s.validation={}
        return s

do_read_input(input_str)

Reads the given input.

Parameters:

Name Type Description Default
input_str str

The input string representing a URL or local path.

required

Returns:

Name Type Description
str str

the input content as a string

Source code in ngwidgets/webserver.py
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
def do_read_input(self, input_str: str) -> str:
    """Reads the given input.

    Args:
        input_str (str): The input string representing a URL or local path.

    Returns:
        str: the input content as a string
    """
    if input_str.startswith("http://") or input_str.startswith("https://"):
        with urllib.request.urlopen(input_str) as response:
            text = response.read().decode("utf-8")
            return text
    else:
        if os.path.exists(input_str):
            with open(input_str, "r") as file:
                return file.read()
        else:
            raise Exception(f"File does not exist: {input_str}")

handle_exception(e, trace=None)

Handles an exception by creating an error message.

Parameters:

Name Type Description Default
e BaseException

The exception to handle.

required
trace bool

Whether to include the traceback in the error message. Default is False.

None
Source code in ngwidgets/webserver.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
def handle_exception(self, e: BaseException, trace: Optional[bool] = None):
    """Handles an exception by creating an error message.

    Args:
        e (BaseException): The exception to handle.
        trace (bool, optional): Whether to include the traceback in the error message. Default is False.
    """
    if trace is None and self.webserver:
        trace = self.webserver.do_trace
    if trace:
        self.error_msg = str(e) + "\n" + traceback.format_exc()
    else:
        self.error_msg = str(e)
    if self.log_view:
        self.log_view.push(self.error_msg)
    print(self.error_msg, file=sys.stderr)

Creates a button with a specified icon that opens a target URL upon being clicked.

Parameters:

Name Type Description Default
name str

The name to be displayed on the button.

required
target str

The target URL that should be opened when the button is clicked.

required
icon_name str

The name of the icon to be displayed on the button.

required
new_tab bool

if True open link in new tab

True

Returns:

Type Description

The button object.

see https://fonts.google.com/icons?icon.set=Material+Icons for potential icon names

Source code in ngwidgets/webserver.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
def link_button(self, name: str, target: str, icon_name: str, new_tab: bool = True):
    """
    Creates a button with a specified icon that opens a target URL upon being clicked.

    Args:
        name (str): The name to be displayed on the button.
        target (str): The target URL that should be opened when the button is clicked.
        icon_name (str): The name of the icon to be displayed on the button.
        new_tab(bool): if True open link in new tab

    Returns:
        The button object.


    see https://fonts.google.com/icons?icon.set=Material+Icons for potential icon names
    """
    with ui.link(text=" ", target=target, new_tab=new_tab) as link_btn:
        link_btn.button = ui.button(name, icon=icon_name)
    return link_btn

notify(msg)

call ui.notify with a context

Source code in ngwidgets/webserver.py
278
279
280
281
282
283
def notify(self, msg: str):
    """
    call ui.notify with a context
    """
    with self.content_div:
        ui.notify(msg)

prepare() async

make sure this solution context is ready for use

Source code in ngwidgets/webserver.py
285
286
287
288
289
290
291
async def prepare(self):
    """
    make sure this solution context is ready for use
    """
    timeout = self.config.timeout
    if timeout is not None:
        await self.client.connected(timeout=timeout)

prepare_ui()

call any code necessary before the first nicegui.ui call is done e.g. handling command line arguments

The base method does nothing and serves as a placeholder for subclasses to define their own UI preparation logic.

Source code in ngwidgets/webserver.py
293
294
295
296
297
298
299
300
def prepare_ui(self):
    """
    call any code necessary before the first nicegui.ui call is
    done e.g. handling command line arguments

    The base method does nothing and serves as a placeholder for subclasses to define their own UI preparation logic.
    """
    pass

round_label(title, background_color='#e6e6e6', **kwargs)

Creates a label with rounded corners and optional background color.

Parameters:

Name Type Description Default
title str

The text to be displayed in the label.

required
background_color str

Hex color code for the label's background. Defaults to a light grey color ("#e6e6e6").

'#e6e6e6'
**kwargs

Additional keyword arguments passed to the select widget creation.

{}

Returns:

Type Description
label

ui.label: A NiceGUI label element with rounded corners and the specified background color.

Source code in ngwidgets/webserver.py
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
def round_label(
    self, title: str, background_color: str = "#e6e6e6", **kwargs
) -> ui.label:
    """
    Creates a label with rounded corners and optional background color.

    Args:
        title (str): The text to be displayed in the label.
        background_color (str): Hex color code for the label's background.
                                Defaults to a light grey color ("#e6e6e6").
        **kwargs: Additional keyword arguments passed to the select widget creation.

    Returns:
        ui.label: A NiceGUI label element with rounded corners and the specified background color.
    """
    background_style = (
        f"background-color: {background_color};" if background_color else ""
    )
    round_label = (
        ui.label(title, **kwargs)
        .classes("rounded p-2")
        .style(f"margin-right: 10px; {background_style}")
    )
    return round_label

run_busy(func, status=None, button=None, spinner=None, on_result=None, busy_text='working ...', done_text='done', timeout=20.0)

run the given blocking function in the background while the user interface shows that it is busy - see https://github.com/WolfgangFahl/nicegui_widgets/issues/100

the busy state is shown at once, the work runs off the event loop and every user interface update happens inside my container

Parameters:

Name Type Description Default
func callable

the blocking function to run

required
status label

the label to show the state in

None
button button

the button to disable while busy

None
spinner spinner

the spinner to show while busy

None
on_result callable

called with the return value of func

None
busy_text str

the status text while busy

'working ...'
done_text str

the status text when finished

'done'
timeout float

the seconds after which the task is cancelled

20.0
Source code in ngwidgets/webserver.py
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def run_busy(
    self,
    func: callable,
    status: ui.label = None,
    button: ui.button = None,
    spinner: ui.spinner = None,
    on_result: callable = None,
    busy_text: str = "working ...",
    done_text: str = "done",
    timeout: float = 20.0,
):
    """
    run the given blocking function in the background while the user
    interface shows that it is busy - see
    https://github.com/WolfgangFahl/nicegui_widgets/issues/100

    the busy state is shown at once, the work runs off the event loop
    and every user interface update happens inside my container

    Args:
        func: the blocking function to run
        status: the label to show the state in
        button: the button to disable while busy
        spinner: the spinner to show while busy
        on_result: called with the return value of func
        busy_text: the status text while busy
        done_text: the status text when finished
        timeout: the seconds after which the task is cancelled
    """
    runner = TaskRunner(timeout=timeout)

    def set_busy(busy: bool, text: str):
        with self.container:
            if button:
                if busy:
                    button.disable()
                else:
                    button.enable()
            if spinner:
                spinner.set_visibility(busy)
            if status:
                status.set_text(text)

    def handle_result(result):
        set_busy(False, done_text)
        if on_result:
            with self.container:
                on_result(result)

    set_busy(True, busy_text)
    runner.run_blocking(func, on_result=handle_result)
    return runner

setup_content_div(setup_content=None, with_exception_handling=True, **kwargs) async

Sets up the content frame div of the web server's user interface.

Parameters:

Name Type Description Default
setup_content Optional[Callable]

A callable for setting up the main content. It can be a regular function or a coroutine.

None
with_exception_handling bool

if True handle exceptions

True
Note

This method is asynchronous and should be awaited when called.

Source code in ngwidgets/webserver.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
async def setup_content_div(
    self,
    setup_content: Optional[Callable] = None,
    with_exception_handling: bool = True,
    **kwargs,
):
    """
    Sets up the content frame div of the web server's user interface.

    Args:
        setup_content (Optional[Callable]): A callable for setting up the main content.
                                             It can be a regular function or a coroutine.
        with_exception_handling(bool): if True handle exceptions

    Note:
        This method is asynchronous and should be awaited when called.
    """
    # Setting up the menu
    self.setup_menu()

    with ui.element("div").classes("w-full h-full") as self.content_div:
        self.container = self.content_div
        # Execute setup_content if provided
        if setup_content:
            try:
                if asyncio.iscoroutinefunction(setup_content):
                    await setup_content(**kwargs)
                else:
                    setup_content(**kwargs)
            except Exception as ex:
                if with_exception_handling:
                    self.handle_exception(ex)
                else:
                    raise ex

    await self.setup_footer()

setup the footer

Source code in ngwidgets/webserver.py
533
534
535
536
537
538
539
async def setup_footer(self):
    """
    setup the footer
    """
    with ui.footer() as self.footer:
        ui.label(self.config.copy_right)
        ui.link("Powered by nicegui", "https://nicegui.io/").style("color: #fff")

setup_menu(detailed=None)

set up the default menu home/settings and about

Parameters:

Name Type Description Default
detailed bool

if True add github,chat and help links

None
Source code in ngwidgets/webserver.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
def setup_menu(self, detailed: bool = None):
    """
    set up the default menu home/settings and about

    Args:
        detailed(bool): if True add github,chat and help links
    """
    self.header = None
    version = self.config.version
    if detailed is None:
        detailed = self.config.detailed_menu
    self.config.color_schema.apply()
    # parent element for hamburger menu
    with ui.element("div").classes(
        "top-0 left-0 z-50 bg-transparent"
    ) as self.hamburger_container:
        self.hamburger_button1 = ui.button(
            icon="menu", on_click=self.toogle_hamburger
        )
        self.hamburger_button1.visible = False
    with ui.header() as self.header:
        self.hamburger_button = ui.button(
            icon="menu", on_click=self.toogle_hamburger
        )
        self.link_button("home", "/", "home", new_tab=False)
        self.link_button("settings", "/settings", "settings", new_tab=False)
        self.configure_menu()
        if detailed:
            self.link_button("github", version.cm_url, "bug_report")
            self.link_button("chat", version.chat_url, "chat")
            self.link_button("help", version.doc_url, "help")
            self.link_button("about", "/about", "info", new_tab=False)

toggle_icon(button)

toggle the icon of the given button

Parameters:

Name Type Description Default
ui.button

the button that needs the icon to be toggled

required
Source code in ngwidgets/webserver.py
401
402
403
404
405
406
407
408
409
410
411
412
413
414
def toggle_icon(self, button: ui.button):
    """
    toggle the icon of the given button

    Args:
        ui.button: the button that needs the icon to be toggled
    """
    if hasattr(button, "toggle_icon"):
        # exchange icon with toggle icon
        toggle_icon = button._props["icon"]
        icon = button.toggle_icon
        button._props["icon"] = icon
        button.toggle_icon = toggle_icon
    button.update()

toogle_hamburger() async

toggle the hamburger menu

Source code in ngwidgets/webserver.py
489
490
491
492
493
494
495
496
497
498
async def toogle_hamburger(self):
    """
    toggle the hamburger menu
    """
    if not self.header:
        return

    self.header.toggle()
    self.footer.toggle()
    self.hamburger_button1.visible = not self.hamburger_button1.visible

tool_button(tooltip, icon, handler=None, toggle_icon=None)

Creates a button with icon that triggers a specified function upon being clicked.

Parameters:

Name Type Description Default
tooltip str

The tooltip to be displayed.

required
icon str

The name of the icon to be displayed on the button.

required
handler function

The function to be called when the button is clicked.

None
toggle_icon str

The name of an alternative icon to be displayed when the button is clicked.

None

Returns:

Type Description
button

ui.button: The icon button object.

valid icons may be found at

https://fonts.google.com/icons

Source code in ngwidgets/webserver.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
def tool_button(
    self, tooltip: str, icon: str, handler: callable = None, toggle_icon: str = None
) -> ui.button:
    """
    Creates a button with icon that triggers a specified function upon being clicked.

    Args:
        tooltip (str): The tooltip to be displayed.
        icon (str): The name of the icon to be displayed on the button.
        handler (function): The function to be called when the button is clicked.
        toggle_icon (str): The name of an alternative icon to be displayed when the button is clicked.

    Returns:
        ui.button: The icon button object.

    valid icons may be found at:
        https://fonts.google.com/icons
    """
    icon_button = (
        ui.button("", icon=icon, color="primary")
        .tooltip(tooltip)
        .on("click", handler=handler)
    )
    icon_button.toggle_icon = toggle_icon
    return icon_button

WebserverConfig

configuration of a webserver

Source code in ngwidgets/webserver.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@lod_storable
class WebserverConfig:
    """
    configuration of a webserver
    """

    # the short name to be used e.g. for determining the default storage_path
    short_name: str

    # set your copyright string here
    copy_right: Optional[str] = ""
    default_port: int = 9860
    version: Optional[Version] = None
    color_schema: ColorSchema = field(default_factory=ColorSchema.indigo)
    fastapi_docs: Optional[bool] = True
    detailed_menu: bool = True
    timeout: Optional[float] = None
    storage_secret: Optional[str] = None
    storage_path: Optional[str] = None
    config_path: Optional[str] = None

    def __post_init__(self):
        """
        make sure the necessary fields exist
        """
        self.config_path = self.base_path
        self.storage_path = self.storage_path or os.path.join(self.base_path, "storage")
        self.storage_secret = self.storage_secret or str(uuid.uuid4())
        self.timeout = self.timeout if self.timeout is not None else 3.0

    @property
    def yaml_path(self) -> str:
        return os.path.join(self.config_path, f"{self.short_name}_config.yaml")

    @property
    def base_path(self) -> str:
        base_path = self.config_path or os.path.join(
            os.path.expanduser("~"), ".solutions", self.short_name
        )
        return base_path

    @classmethod
    def get(cls, config: "WebserverConfig") -> "WebserverConfig":
        """
        Retrieves or initializes a WebserverConfig instance based on the provided 'config' parameter.
        This method ensures that essential properties like 'storage_secret', 'config_path', and 'storage_path'
        are set in the 'config' object. If a configuration file already exists at the 'yaml_path', it's loaded,
        and its values are used to update the provided 'config'. However, certain key properties like 'version',
        'short_name', and 'default_port' can still be overridden by the provided 'config' if they are set.

        If the configuration file does not exist, this method will create the necessary directories and save
        the provided 'config' as the initial configuration to the 'yaml_path', which is derived from 'config_path'
        and 'short_name' and typically located under the user's home directory in the '.solutions' folder.

        Args:
            config (WebserverConfig): The configuration object with preferred or default settings.

        Returns:
            WebserverConfig: The configuration loaded from the YAML file, or the provided 'config'
                             if the YAML file does not exist.
        """
        if os.path.exists(config.yaml_path):
            # Load the existing config
            server_config = cls.load_from_yaml_file(config.yaml_path)
            if config.version:
                server_config.version = config.version
            if config.copy_right:
                server_config.copy_right = config.copy_right
            if config.default_port != 9680:
                server_config.default_port = config.default_port
            if config.short_name != server_config.short_name:
                _msg = f"config short_name mismatch {config.short_name}!={server_config.short_name}"
                pass
            server_config.short_name = config.short_name
        else:
            # Create the directories to make sure they  exist
            os.makedirs(config.config_path, exist_ok=True)
            os.makedirs(config.storage_path, exist_ok=True)

            # Use the provided default_config as the initial configuration
            server_config = config
            server_config.save_to_yaml_file(config.yaml_path)

        return server_config

__post_init__()

make sure the necessary fields exist

Source code in ngwidgets/webserver.py
46
47
48
49
50
51
52
53
def __post_init__(self):
    """
    make sure the necessary fields exist
    """
    self.config_path = self.base_path
    self.storage_path = self.storage_path or os.path.join(self.base_path, "storage")
    self.storage_secret = self.storage_secret or str(uuid.uuid4())
    self.timeout = self.timeout if self.timeout is not None else 3.0

get(config) classmethod

Retrieves or initializes a WebserverConfig instance based on the provided 'config' parameter. This method ensures that essential properties like 'storage_secret', 'config_path', and 'storage_path' are set in the 'config' object. If a configuration file already exists at the 'yaml_path', it's loaded, and its values are used to update the provided 'config'. However, certain key properties like 'version', 'short_name', and 'default_port' can still be overridden by the provided 'config' if they are set.

If the configuration file does not exist, this method will create the necessary directories and save the provided 'config' as the initial configuration to the 'yaml_path', which is derived from 'config_path' and 'short_name' and typically located under the user's home directory in the '.solutions' folder.

Parameters:

Name Type Description Default
config WebserverConfig

The configuration object with preferred or default settings.

required

Returns:

Name Type Description
WebserverConfig WebserverConfig

The configuration loaded from the YAML file, or the provided 'config' if the YAML file does not exist.

Source code in ngwidgets/webserver.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@classmethod
def get(cls, config: "WebserverConfig") -> "WebserverConfig":
    """
    Retrieves or initializes a WebserverConfig instance based on the provided 'config' parameter.
    This method ensures that essential properties like 'storage_secret', 'config_path', and 'storage_path'
    are set in the 'config' object. If a configuration file already exists at the 'yaml_path', it's loaded,
    and its values are used to update the provided 'config'. However, certain key properties like 'version',
    'short_name', and 'default_port' can still be overridden by the provided 'config' if they are set.

    If the configuration file does not exist, this method will create the necessary directories and save
    the provided 'config' as the initial configuration to the 'yaml_path', which is derived from 'config_path'
    and 'short_name' and typically located under the user's home directory in the '.solutions' folder.

    Args:
        config (WebserverConfig): The configuration object with preferred or default settings.

    Returns:
        WebserverConfig: The configuration loaded from the YAML file, or the provided 'config'
                         if the YAML file does not exist.
    """
    if os.path.exists(config.yaml_path):
        # Load the existing config
        server_config = cls.load_from_yaml_file(config.yaml_path)
        if config.version:
            server_config.version = config.version
        if config.copy_right:
            server_config.copy_right = config.copy_right
        if config.default_port != 9680:
            server_config.default_port = config.default_port
        if config.short_name != server_config.short_name:
            _msg = f"config short_name mismatch {config.short_name}!={server_config.short_name}"
            pass
        server_config.short_name = config.short_name
    else:
        # Create the directories to make sure they  exist
        os.makedirs(config.config_path, exist_ok=True)
        os.makedirs(config.storage_path, exist_ok=True)

        # Use the provided default_config as the initial configuration
        server_config = config
        server_config.save_to_yaml_file(config.yaml_path)

    return server_config

webserver_test

Created on 2023-11-03

@author: wf

WebserverTest

Bases: BaseWebserverTest

a webserver test environment

Attributes:

Name Type Description
ws

An instance of the web server being tested.

ws_thread

The thread running the web server.

client

A test client for interacting with the web server.

Source code in ngwidgets/webserver_test.py
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
class WebserverTest(BaseWebserverTest):
    """
    a webserver test environment

    Attributes:
        ws: An instance of the web server being tested.
        ws_thread: The thread running the web server.
        client: A test client for interacting with the web server.
    """

    def setUp(self, server_class, cmd_class, debug=False, profile=True):
        """
        Create and start a test instance of a web server using the specified server and command classes.

        Args:
            server_class: The class of the server to be tested. This should be a class reference that
                          includes a static `get_config()` method and an instance method `run()`.
            cmd_class: The command class used to parse command-line arguments for the server.
                       This class should have an initialization accepting `config` and `server_class`
                       and a method `cmd_parse()` that accepts a list of arguments.

        Returns:
            An instance of WebserverTest containing the started server thread and test client.

        Raises:
            ValueError: If an invalid server_class or cmd_class is provided.

        Example:
            >>> test_server = WebserverTest.get_webserver_test(MyServerClass, MyCommandClass)
            >>> test_server.client.get('/')
            <Response [200]>
        """
        Basetest.setUp(self, debug=debug, profile=profile)
        self.config = (
            server_class.get_config()
        )  # Assumes `get_config()` is a class method of server_class
        self.config.default_port += (
            10000  # Use a different port for testing than for production
        )

        self.cmd = cmd_class(
            self.config, server_class
        )  # Instantiate the command class with config and server_class
        argv = []
        args = self.cmd.parse_args(
            argv
        )  # Parse the command-line arguments with no arguments passed

        self.ws = server_class()  # Instantiate the server class
        self.server_runner = ThreadedServerRunner(self.ws, args=args, debug=self.debug)
        self.server_runner.start()  # start server in separate thread

        self.client = TestClient(
            self.ws.app
        )  # Instantiate the test client with the server's app

    def tearDown(self):
        """
        tear Down everything
        """
        super().tearDown()
        # Stop the server using the ThreadedServerRunner
        self.server_runner.cancel_pending_tasks()
        self.server_runner.stop()

    def get_response(self, path: str, expected_status_code: int = 200) -> Any:
        """Get response using TestClient"""
        response = self.client.get(path)
        self.assertEqual(response.status_code, expected_status_code)
        return response

    def getHtml(self, path: str) -> str:
        """
        Alias for get_html with default status code

        Args:
            path: The path to request

        Returns:
            The HTML content as a string
        """
        return self.get_html(path)

    def get_html_for_post(self, path: str, data) -> str:
        """
        get the html content for the given path by posting the given data
        """
        response = self.client.post(path, json=data)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(response.content is not None)
        html = response.content.decode()
        if self.debug:
            print(html)
        return html

getHtml(path)

Alias for get_html with default status code

Parameters:

Name Type Description Default
path str

The path to request

required

Returns:

Type Description
str

The HTML content as a string

Source code in ngwidgets/webserver_test.py
86
87
88
89
90
91
92
93
94
95
96
def getHtml(self, path: str) -> str:
    """
    Alias for get_html with default status code

    Args:
        path: The path to request

    Returns:
        The HTML content as a string
    """
    return self.get_html(path)

get_html_for_post(path, data)

get the html content for the given path by posting the given data

Source code in ngwidgets/webserver_test.py
 98
 99
100
101
102
103
104
105
106
107
108
def get_html_for_post(self, path: str, data) -> str:
    """
    get the html content for the given path by posting the given data
    """
    response = self.client.post(path, json=data)
    self.assertEqual(response.status_code, 200)
    self.assertTrue(response.content is not None)
    html = response.content.decode()
    if self.debug:
        print(html)
    return html

get_response(path, expected_status_code=200)

Get response using TestClient

Source code in ngwidgets/webserver_test.py
80
81
82
83
84
def get_response(self, path: str, expected_status_code: int = 200) -> Any:
    """Get response using TestClient"""
    response = self.client.get(path)
    self.assertEqual(response.status_code, expected_status_code)
    return response

setUp(server_class, cmd_class, debug=False, profile=True)

Create and start a test instance of a web server using the specified server and command classes.

Parameters:

Name Type Description Default
server_class

The class of the server to be tested. This should be a class reference that includes a static get_config() method and an instance method run().

required
cmd_class

The command class used to parse command-line arguments for the server. This class should have an initialization accepting config and server_class and a method cmd_parse() that accepts a list of arguments.

required

Returns:

Type Description

An instance of WebserverTest containing the started server thread and test client.

Raises:

Type Description
ValueError

If an invalid server_class or cmd_class is provided.

Example

test_server = WebserverTest.get_webserver_test(MyServerClass, MyCommandClass) test_server.client.get('/')

Source code in ngwidgets/webserver_test.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def setUp(self, server_class, cmd_class, debug=False, profile=True):
    """
    Create and start a test instance of a web server using the specified server and command classes.

    Args:
        server_class: The class of the server to be tested. This should be a class reference that
                      includes a static `get_config()` method and an instance method `run()`.
        cmd_class: The command class used to parse command-line arguments for the server.
                   This class should have an initialization accepting `config` and `server_class`
                   and a method `cmd_parse()` that accepts a list of arguments.

    Returns:
        An instance of WebserverTest containing the started server thread and test client.

    Raises:
        ValueError: If an invalid server_class or cmd_class is provided.

    Example:
        >>> test_server = WebserverTest.get_webserver_test(MyServerClass, MyCommandClass)
        >>> test_server.client.get('/')
        <Response [200]>
    """
    Basetest.setUp(self, debug=debug, profile=profile)
    self.config = (
        server_class.get_config()
    )  # Assumes `get_config()` is a class method of server_class
    self.config.default_port += (
        10000  # Use a different port for testing than for production
    )

    self.cmd = cmd_class(
        self.config, server_class
    )  # Instantiate the command class with config and server_class
    argv = []
    args = self.cmd.parse_args(
        argv
    )  # Parse the command-line arguments with no arguments passed

    self.ws = server_class()  # Instantiate the server class
    self.server_runner = ThreadedServerRunner(self.ws, args=args, debug=self.debug)
    self.server_runner.start()  # start server in separate thread

    self.client = TestClient(
        self.ws.app
    )  # Instantiate the test client with the server's app

tearDown()

tear Down everything

Source code in ngwidgets/webserver_test.py
71
72
73
74
75
76
77
78
def tearDown(self):
    """
    tear Down everything
    """
    super().tearDown()
    # Stop the server using the ThreadedServerRunner
    self.server_runner.cancel_pending_tasks()
    self.server_runner.stop()

widgets

Created on 2023-09-10

common nicegui widgets and tools

@author: wf

About

Bases: element

About Div for a given version

Source code in ngwidgets/widgets.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
class About(ui.element):
    """
    About Div for a given version
    """

    def __init__(
        self,
        version,
        font_size=24,
        font_family="Helvetica, Arial, sans-serif",
        **kwargs,
    ):
        """
        construct an about Div for the given version
        """

        def add(html, l, code):
            html += f'<div class="about_row"><div class="about_column1">{l}:</div><div class="about_column2">{code}</div></div>'
            return html

        super().__init__(tag="div", **kwargs)
        with self:
            doc_link = Link.create(
                url=version.doc_url, text="documentation", target="_blank"
            )
            disc_link = Link.create(
                url=version.chat_url, text="discussion", target="_blank"
            )
            cm_link = Link.create(url=version.cm_url, text="source", target="_blank")
            max_label_length = 7  # e.g. updated
            column1_width = (
                font_size * max_label_length
            )  # Approximate width calculation

            html = f"""<style>
                    .about_row {{
                        display: flex;
                        align-items: baseline;
                    }}
                    .about_column1 {{
                        font-weight: bold;
                        font-size: {font_size}px;
                        text-align: right;
                        width: {column1_width}px; 
                        padding-right: 10px;
                        font-family: {font_family};
                    }}
                    .about_column2 {{
                        font-size: {font_size}px;
                        font-family: {font_family};
                    }}
                    .about_column2 a {{
                        color: blue;
                        text-decoration: underline;
                    }}
               </style>"""
            html = add(html, "name", f"{version.name}")
            html = add(html, "purpose", f"{version.description}")
            html = add(html, "version", f"{version.version}")
            html = add(html, "since", f"{version.date}")
            html = add(html, "updated", f"{version.updated}")
            html = add(html, "authors", f"{version.authors}")
            html = add(html, "docs", doc_link)
            html = add(html, "chat", disc_link)
            html = add(html, "source", cm_link)
            ui.html(html)

__init__(version, font_size=24, font_family='Helvetica, Arial, sans-serif', **kwargs)

construct an about Div for the given version

Source code in ngwidgets/widgets.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __init__(
    self,
    version,
    font_size=24,
    font_family="Helvetica, Arial, sans-serif",
    **kwargs,
):
    """
    construct an about Div for the given version
    """

    def add(html, l, code):
        html += f'<div class="about_row"><div class="about_column1">{l}:</div><div class="about_column2">{code}</div></div>'
        return html

    super().__init__(tag="div", **kwargs)
    with self:
        doc_link = Link.create(
            url=version.doc_url, text="documentation", target="_blank"
        )
        disc_link = Link.create(
            url=version.chat_url, text="discussion", target="_blank"
        )
        cm_link = Link.create(url=version.cm_url, text="source", target="_blank")
        max_label_length = 7  # e.g. updated
        column1_width = (
            font_size * max_label_length
        )  # Approximate width calculation

        html = f"""<style>
                .about_row {{
                    display: flex;
                    align-items: baseline;
                }}
                .about_column1 {{
                    font-weight: bold;
                    font-size: {font_size}px;
                    text-align: right;
                    width: {column1_width}px; 
                    padding-right: 10px;
                    font-family: {font_family};
                }}
                .about_column2 {{
                    font-size: {font_size}px;
                    font-family: {font_family};
                }}
                .about_column2 a {{
                    color: blue;
                    text-decoration: underline;
                }}
           </style>"""
        html = add(html, "name", f"{version.name}")
        html = add(html, "purpose", f"{version.description}")
        html = add(html, "version", f"{version.version}")
        html = add(html, "since", f"{version.date}")
        html = add(html, "updated", f"{version.updated}")
        html = add(html, "authors", f"{version.authors}")
        html = add(html, "docs", doc_link)
        html = add(html, "chat", disc_link)
        html = add(html, "source", cm_link)
        ui.html(html)

HideShow

A class representing a hideable/showable section in a NiceGUI application.

Source code in ngwidgets/widgets.py
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
class HideShow:
    """
    A class representing a hideable/showable section in a NiceGUI application.
    """

    TRIANGLE_LEFT = "◀"
    TRIANGLE_DOWN = "▼"

    def __init__(
        self,
        hide_show_label: tuple[str, str] = None,
        show_content: bool = True,
        content_div=None,
        lazy_init: bool = False,
        **kwargs,
    ):
        """
        Initialize the HideShow component.

        Args:
            hide_show_label: Labels for shown/hidden states.
            show_content: Initial visibility state (default True).
            content_div: Div with content to hide/show.
            lazy_init: If True, content_div initialized later.
            **kwargs: Additional args for button element.
        """
        self.label_if_shown, self.label_if_hidden = hide_show_label
        self.show_content = show_content
        self.btn = ui.button(
            text=self._get_status_label(show_content),
            on_click=self.toggle_hide_show,
            **kwargs,
        )

        if not lazy_init:
            self.content_div = content_div if content_div else ui.element()
        else:
            self.content_div = None

        self._set_show_content(show_content)

    def _set_show_content(self, show_content: bool):
        """
        Set visibility of content.
        """
        if self.content_div:
            self.show_content = show_content
            self.content_div.set_visibility(show_content)
            self.btn.set_text(self._get_status_label(show_content))

    def _get_status_label(self, show_content: bool) -> str:
        """
        Get label text based on visibility state.
        """
        icon = self.TRIANGLE_DOWN if show_content else self.TRIANGLE_LEFT
        label = (
            self.label_if_shown
            if show_content
            else (self.label_if_hidden if self.label_if_hidden else self.label_if_shown)
        )
        return f"{label} {icon}"

    def toggle_hide_show(self, _=None):
        """
        Toggle visibility of content.
        """
        self._set_show_content(not self.show_content)

    def add(self, widget):
        """
        Add a widget to content div.
        """
        self.content_div.add(widget)

    def update(self):
        """
        Update the content div.
        """
        self.content_div.update()

    def set_content(self, content_div=None):
        """
        Set or update content div.
        """
        self.content_div = content_div if content_div else ui.element()
        self._set_show_content(self.show_content)

__init__(hide_show_label=None, show_content=True, content_div=None, lazy_init=False, **kwargs)

Initialize the HideShow component.

Parameters:

Name Type Description Default
hide_show_label tuple[str, str]

Labels for shown/hidden states.

None
show_content bool

Initial visibility state (default True).

True
content_div

Div with content to hide/show.

None
lazy_init bool

If True, content_div initialized later.

False
**kwargs

Additional args for button element.

{}
Source code in ngwidgets/widgets.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def __init__(
    self,
    hide_show_label: tuple[str, str] = None,
    show_content: bool = True,
    content_div=None,
    lazy_init: bool = False,
    **kwargs,
):
    """
    Initialize the HideShow component.

    Args:
        hide_show_label: Labels for shown/hidden states.
        show_content: Initial visibility state (default True).
        content_div: Div with content to hide/show.
        lazy_init: If True, content_div initialized later.
        **kwargs: Additional args for button element.
    """
    self.label_if_shown, self.label_if_hidden = hide_show_label
    self.show_content = show_content
    self.btn = ui.button(
        text=self._get_status_label(show_content),
        on_click=self.toggle_hide_show,
        **kwargs,
    )

    if not lazy_init:
        self.content_div = content_div if content_div else ui.element()
    else:
        self.content_div = None

    self._set_show_content(show_content)

add(widget)

Add a widget to content div.

Source code in ngwidgets/widgets.py
301
302
303
304
305
def add(self, widget):
    """
    Add a widget to content div.
    """
    self.content_div.add(widget)

set_content(content_div=None)

Set or update content div.

Source code in ngwidgets/widgets.py
313
314
315
316
317
318
def set_content(self, content_div=None):
    """
    Set or update content div.
    """
    self.content_div = content_div if content_div else ui.element()
    self._set_show_content(self.show_content)

toggle_hide_show(_=None)

Toggle visibility of content.

Source code in ngwidgets/widgets.py
295
296
297
298
299
def toggle_hide_show(self, _=None):
    """
    Toggle visibility of content.
    """
    self._set_show_content(not self.show_content)

update()

Update the content div.

Source code in ngwidgets/widgets.py
307
308
309
310
311
def update(self):
    """
    Update the content div.
    """
    self.content_div.update()

Lang

A class representing languages, providing utility methods related to language data.

Source code in ngwidgets/widgets.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
class Lang:
    """
    A class representing languages, providing utility methods related to language data.
    """

    @classmethod
    def get_language_dict(cls) -> dict:
        """
        Get a dictionary of supported languages.

        Returns:
            dict[str, str]: A dictionary where the keys are language codes (e.g., "en" for English)
                            and the values are the corresponding names/representations in HTML entity format.
        """
        # see https://github.com/sahajsk21/Anvesha/blob/master/src/components/topnav.js
        languages_list = [
            ["ar", "&#1575;&#1604;&#1593;&#1585;&#1576;&#1610;&#1577;"],
            ["arz", "&#1605;&#1589;&#1585;&#1609;"],
            ["ast", "Asturianu"],
            ["az", "Az&#601;rbaycanca"],
            ["azb", "&#1578;&#1734;&#1585;&#1705;&#1580;&#1607;"],
            [
                "be",
                "&#1041;&#1077;&#1083;&#1072;&#1088;&#1091;&#1089;&#1082;&#1072;&#1103;",
            ],
            ["bg", "&#1041;&#1098;&#1083;&#1075;&#1072;&#1088;&#1089;&#1082;&#1080;"],
            ["bn", "&#2476;&#2494;&#2434;&#2482;&#2494;"],
            ["ca", "Catal&agrave;"],
            ["ce", "&#1053;&#1086;&#1093;&#1095;&#1080;&#1081;&#1085;"],
            ["ceb", "Sinugboanong Binisaya"],
            ["cs", "&#268;e&scaron;tina"],
            ["cy", "Cymraeg"],
            ["da", "Dansk"],
            ["de", "Deutsch"],
            ["el", "&Epsilon;&lambda;&lambda;&eta;&nu;&iota;&kappa;&#940;"],
            ["en", "English"],
            ["eo", "Esperanto"],
            ["es", "Espa&ntilde;ol"],
            ["et", "Eesti"],
            ["eu", "Euskara"],
            ["fa", "&#1601;&#1575;&#1585;&#1587;&#1740;"],
            ["fi", "Suomi"],
            ["fr", "Fran&ccedil;ais"],
            ["gl", "Galego"],
            ["he", "&#1506;&#1489;&#1512;&#1497;&#1514;"],
            ["hi", "&#2361;&#2367;&#2344;&#2381;&#2342;&#2368;"],
            ["hr", "Hrvatski"],
            ["hu", "Magyar"],
            ["hy", "&#1344;&#1377;&#1397;&#1381;&#1408;&#1381;&#1398;"],
            ["id", "Bahasa Indonesia"],
            ["it", "Italiano"],
            ["ja", "&#26085;&#26412;&#35486;"],
            ["ka", "&#4325;&#4304;&#4320;&#4311;&#4323;&#4314;&#4312;"],
            [
                "kk",
                "&#1178;&#1072;&#1079;&#1072;&#1179;&#1096;&#1072; / Qazaq&#351;a / &#1602;&#1575;&#1586;&#1575;&#1602;&#1588;&#1575;",
            ],
            ["ko", "&#54620;&#44397;&#50612;"],
            ["la", "Latina"],
            ["lt", "Lietuvi&#371;"],
            ["lv", "Latvie&scaron;u"],
            ["min", "Bahaso Minangkabau"],
            ["ms", "Bahasa Melayu"],
            ["nan", "B&acirc;n-l&acirc;m-g&uacute; / H&#333;-l&oacute;-o&#275;"],
            ["nb", "Norsk (bokm&aring;l)"],
            ["nl", "Nederlands"],
            ["nn", "Norsk (nynorsk)"],
            ["pl", "Polski"],
            ["pt", "Portugu&ecirc;s"],
            ["ro", "Rom&acirc;n&#259;"],
            ["ru", "&#1056;&#1091;&#1089;&#1089;&#1082;&#1080;&#1081;"],
            [
                "sh",
                "Srpskohrvatski / &#1057;&#1088;&#1087;&#1089;&#1082;&#1086;&#1093;&#1088;&#1074;&#1072;&#1090;&#1089;&#1082;&#1080;",
            ],
            ["sk", "Sloven&#269;ina"],
            ["sl", "Sloven&scaron;&#269;ina"],
            ["sr", "&#1057;&#1088;&#1087;&#1089;&#1082;&#1080; / Srpski"],
            ["sv", "Svenska"],
            ["ta", "&#2980;&#2990;&#3007;&#2996;&#3021;"],
            ["tg", "&#1058;&#1086;&#1207;&#1080;&#1082;&#1251;"],
            ["th", "&#3616;&#3634;&#3625;&#3634;&#3652;&#3607;&#3618;"],
            ["tr", "T&uuml;rk&ccedil;e"],
            [
                "tt",
                "&#1058;&#1072;&#1090;&#1072;&#1088;&#1095;&#1072; / Tatar&ccedil;a",
            ],
            [
                "uk",
                "&#1059;&#1082;&#1088;&#1072;&#1111;&#1085;&#1089;&#1100;&#1082;&#1072;",
            ],
            ["ur", "&#1575;&#1585;&#1583;&#1608;"],
            [
                "uz",
                "O&#699;zbekcha / &#1038;&#1079;&#1073;&#1077;&#1082;&#1095;&#1072;",
            ],
            ["vi", "Ti&#7871;ng Vi&#7879;t"],
            ["vo", "Volap&uuml;k"],
            ["war", "Winaray"],
            ["yue", "&#31925;&#35486;"],
            ["zh", "&#20013;&#25991;"],
        ]
        languages_dict = {}
        for code, desc in languages_list:
            desc = html.unescape(desc)
            languages_dict[code] = desc

        return languages_dict

get_language_dict() classmethod

Get a dictionary of supported languages.

Returns:

Type Description
dict

dict[str, str]: A dictionary where the keys are language codes (e.g., "en" for English) and the values are the corresponding names/representations in HTML entity format.

Source code in ngwidgets/widgets.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@classmethod
def get_language_dict(cls) -> dict:
    """
    Get a dictionary of supported languages.

    Returns:
        dict[str, str]: A dictionary where the keys are language codes (e.g., "en" for English)
                        and the values are the corresponding names/representations in HTML entity format.
    """
    # see https://github.com/sahajsk21/Anvesha/blob/master/src/components/topnav.js
    languages_list = [
        ["ar", "&#1575;&#1604;&#1593;&#1585;&#1576;&#1610;&#1577;"],
        ["arz", "&#1605;&#1589;&#1585;&#1609;"],
        ["ast", "Asturianu"],
        ["az", "Az&#601;rbaycanca"],
        ["azb", "&#1578;&#1734;&#1585;&#1705;&#1580;&#1607;"],
        [
            "be",
            "&#1041;&#1077;&#1083;&#1072;&#1088;&#1091;&#1089;&#1082;&#1072;&#1103;",
        ],
        ["bg", "&#1041;&#1098;&#1083;&#1075;&#1072;&#1088;&#1089;&#1082;&#1080;"],
        ["bn", "&#2476;&#2494;&#2434;&#2482;&#2494;"],
        ["ca", "Catal&agrave;"],
        ["ce", "&#1053;&#1086;&#1093;&#1095;&#1080;&#1081;&#1085;"],
        ["ceb", "Sinugboanong Binisaya"],
        ["cs", "&#268;e&scaron;tina"],
        ["cy", "Cymraeg"],
        ["da", "Dansk"],
        ["de", "Deutsch"],
        ["el", "&Epsilon;&lambda;&lambda;&eta;&nu;&iota;&kappa;&#940;"],
        ["en", "English"],
        ["eo", "Esperanto"],
        ["es", "Espa&ntilde;ol"],
        ["et", "Eesti"],
        ["eu", "Euskara"],
        ["fa", "&#1601;&#1575;&#1585;&#1587;&#1740;"],
        ["fi", "Suomi"],
        ["fr", "Fran&ccedil;ais"],
        ["gl", "Galego"],
        ["he", "&#1506;&#1489;&#1512;&#1497;&#1514;"],
        ["hi", "&#2361;&#2367;&#2344;&#2381;&#2342;&#2368;"],
        ["hr", "Hrvatski"],
        ["hu", "Magyar"],
        ["hy", "&#1344;&#1377;&#1397;&#1381;&#1408;&#1381;&#1398;"],
        ["id", "Bahasa Indonesia"],
        ["it", "Italiano"],
        ["ja", "&#26085;&#26412;&#35486;"],
        ["ka", "&#4325;&#4304;&#4320;&#4311;&#4323;&#4314;&#4312;"],
        [
            "kk",
            "&#1178;&#1072;&#1079;&#1072;&#1179;&#1096;&#1072; / Qazaq&#351;a / &#1602;&#1575;&#1586;&#1575;&#1602;&#1588;&#1575;",
        ],
        ["ko", "&#54620;&#44397;&#50612;"],
        ["la", "Latina"],
        ["lt", "Lietuvi&#371;"],
        ["lv", "Latvie&scaron;u"],
        ["min", "Bahaso Minangkabau"],
        ["ms", "Bahasa Melayu"],
        ["nan", "B&acirc;n-l&acirc;m-g&uacute; / H&#333;-l&oacute;-o&#275;"],
        ["nb", "Norsk (bokm&aring;l)"],
        ["nl", "Nederlands"],
        ["nn", "Norsk (nynorsk)"],
        ["pl", "Polski"],
        ["pt", "Portugu&ecirc;s"],
        ["ro", "Rom&acirc;n&#259;"],
        ["ru", "&#1056;&#1091;&#1089;&#1089;&#1082;&#1080;&#1081;"],
        [
            "sh",
            "Srpskohrvatski / &#1057;&#1088;&#1087;&#1089;&#1082;&#1086;&#1093;&#1088;&#1074;&#1072;&#1090;&#1089;&#1082;&#1080;",
        ],
        ["sk", "Sloven&#269;ina"],
        ["sl", "Sloven&scaron;&#269;ina"],
        ["sr", "&#1057;&#1088;&#1087;&#1089;&#1082;&#1080; / Srpski"],
        ["sv", "Svenska"],
        ["ta", "&#2980;&#2990;&#3007;&#2996;&#3021;"],
        ["tg", "&#1058;&#1086;&#1207;&#1080;&#1082;&#1251;"],
        ["th", "&#3616;&#3634;&#3625;&#3634;&#3652;&#3607;&#3618;"],
        ["tr", "T&uuml;rk&ccedil;e"],
        [
            "tt",
            "&#1058;&#1072;&#1090;&#1072;&#1088;&#1095;&#1072; / Tatar&ccedil;a",
        ],
        [
            "uk",
            "&#1059;&#1082;&#1088;&#1072;&#1111;&#1085;&#1089;&#1100;&#1082;&#1072;",
        ],
        ["ur", "&#1575;&#1585;&#1583;&#1608;"],
        [
            "uz",
            "O&#699;zbekcha / &#1038;&#1079;&#1073;&#1077;&#1082;&#1095;&#1072;",
        ],
        ["vi", "Ti&#7871;ng Vi&#7879;t"],
        ["vo", "Volap&uuml;k"],
        ["war", "Winaray"],
        ["yue", "&#31925;&#35486;"],
        ["zh", "&#20013;&#25991;"],
    ]
    languages_dict = {}
    for code, desc in languages_list:
        desc = html.unescape(desc)
        languages_dict[code] = desc

    return languages_dict

a link

Source code in ngwidgets/widgets.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
class Link:
    """
    a link
    """

    red = "color: red;text-decoration: underline;"
    blue = "color: blue;text-decoration: underline;"

    @staticmethod
    def create(
        url, text, tooltip=None, target=None, style: str = None, url_encode=False
    ):
        """
        Create a link for the given URL and text, with optional URL encoding.

        Args:
            url (str): The URL.
            text (str): The link text.
            tooltip (str): An optional tooltip.
            target (str): Target attribute, e.g., _blank for opening the link in a new tab.
            style (str): CSS style to be applied.
            url_encode (bool): Flag to indicate if the URL needs encoding. default: False

        Returns:
            str: HTML anchor tag as a string.
        """
        if url_encode:
            url = quote(url)

        title = "" if tooltip is None else f" title='{tooltip}'"
        target = "" if target is None else f" target='{target}'"
        if style is None:
            style = Link.blue
        style = f" style='{style}'"
        link = f"<a href='{url}'{title}{target}{style}>{text}</a>"
        return link

create(url, text, tooltip=None, target=None, style=None, url_encode=False) staticmethod

Create a link for the given URL and text, with optional URL encoding.

Parameters:

Name Type Description Default
url str

The URL.

required
text str

The link text.

required
tooltip str

An optional tooltip.

None
target str

Target attribute, e.g., _blank for opening the link in a new tab.

None
style str

CSS style to be applied.

None
url_encode bool

Flag to indicate if the URL needs encoding. default: False

False

Returns:

Name Type Description
str

HTML anchor tag as a string.

Source code in ngwidgets/widgets.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
@staticmethod
def create(
    url, text, tooltip=None, target=None, style: str = None, url_encode=False
):
    """
    Create a link for the given URL and text, with optional URL encoding.

    Args:
        url (str): The URL.
        text (str): The link text.
        tooltip (str): An optional tooltip.
        target (str): Target attribute, e.g., _blank for opening the link in a new tab.
        style (str): CSS style to be applied.
        url_encode (bool): Flag to indicate if the URL needs encoding. default: False

    Returns:
        str: HTML anchor tag as a string.
    """
    if url_encode:
        url = quote(url)

    title = "" if tooltip is None else f" title='{tooltip}'"
    target = "" if target is None else f" target='{target}'"
    if style is None:
        style = Link.blue
    style = f" style='{style}'"
    link = f"<a href='{url}'{title}{target}{style}>{text}</a>"
    return link

widgets_demo

Created on 2023-09-13

@author: wf

NiceGuiWidgetsDemo

Bases: InputWebSolution

Demonstration Solution

Source code in ngwidgets/widgets_demo.py
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
class NiceGuiWidgetsDemo(InputWebSolution):
    """
    Demonstration Solution
    """

    def __init__(self, webserver: "NiceGuiWidgetsDemoWebserver", client: Client):
        """
        Initialize the NiceGuiWidgetsDemoContext.

        Calls the constructor of the base class ClientWebContext to ensure proper initialization
        and then performs any additional setup specific to NiceGuiWidgetsDemoContext.

        Args:
            webserver (NiceGuiWebserver): The webserver instance associated with this context.
            client (Client): The client instance this context is associated with.
        """
        super().__init__(webserver, client)  # Call to the superclass constructor
        self.projects = self.webserver.projects
        # pdf_url = "https://www.africau.edu/images/default/sample.pdf"
        self.pdf_url = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"

    async def load_pdf(self):
        self.pdf_viewer.load_pdf(self.pdf_url)

    #    slider = ui.slider(min=1, max=max_pages, value=1)  # PDF pages usually start from 1
    #    slider_label = ui.label().bind_text_from(slider, 'value')
    # def update_page(e):
    #    viewer.set_page(e.value)
    async def show_components(self, solution_id):
        def show():
            project = self.projects.get_project4_solution_id(solution_id)
            # Create a ComponentsView and display the components
            components_view = ComponentsView(self, self.projects, project)
            components_view.setup()

        await self.setup_content_div(show)

    async def show_solutions(self):
        def show():
            self.projects_view = ProjectsView(self)

        await self.setup_content_div(show)

    async def show_progress(self):
        def show():
            self.progress_bar = NiceguiProgressbar(
                total=100, desc="working", unit="step"
            )
            self.progress_bar.progress.visible = True

            def update_progress(step=1):
                # Update the progress
                self.progress_bar.update(step)
                # Reset the progress bar if it reaches the total
                if self.progress_bar.value >= self.progress_bar.total:
                    self.progress_bar.reset()

            def toggle_auto():
                if not hasattr(self, "auto_timer"):
                    self.auto_timer = ui.timer(
                        interval=0.3, callback=lambda: update_progress(), active=True
                    )
                else:
                    self.auto_timer.active = not self.auto_timer.active

            # Buttons for controlling the progress bar
            with ui.row():
                ui.button("--").on_click(lambda: update_progress(-1))
                ui.button("++").on_click(lambda: update_progress(1))
                ui.button("Auto", on_click=toggle_auto)

        await self.setup_content_div(show)

    async def show_color_schema(self):
        def show():
            self.config.color_schema.display()
            pass

        await self.setup_content_div(show)

    async def show_issue_1786(self):
        """
        https://github.com/zauberzeug/nicegui/discussions/1786
        """

        def foreground_no_slot(event):
            ui.notify(f"{event.sender.text} ed")

        async def background_no_slot(event):
            await run.io_bound(foreground_no_slot, event)

        def foreground_with_slot(event):
            with self.button_row:
                ui.notify(f"{event.sender.text} ed")

        async def background_with_slot(event):
            await run.io_bound(foreground_with_slot, event)

        def show():
            with ui.row() as self.button_row:
                ui.button("foreground no slot", on_=foreground_no_slot)
                ui.button("background no slot", on_=background_no_slot)
                ui.button("foreground with slot", on_=foreground_with_slot)
                ui.button("background with slot", on_=background_with_slot)

        await self.setup_content_div(show)

    async def show_langs(self):
        """
        show languages selection
        """

        def show():
            # Default language set to English
            default_lang = "en"

            # Get available languages
            languages = Lang.get_language_dict()
            with ui.card().style("width: 12%"):
                with ui.row():
                    ui.label("Lang code:")
                    # Create a label to display the chosen language with the default language
                    lang_label = ui.label(default_lang)
                with ui.row():
                    ui.label("Select:")
                    # Create a dropdown for language selection with the default language selected
                    # Bind the label text to the selection's value, so it updates automatically
                    ui.select(languages, value=default_lang).bind_value(
                        lang_label, "text"
                    )

        await self.setup_content_div(show)

    async def show_pdf_viewer(self):
        def show():
            self.pdf_viewer = pdfviewer(debug=self.args.debug).classes("w-full h-96")
            self.tool_button(tooltip="reload", icon="refresh", handler=self.load_pdf)

        await self.setup_content_div(show)

    async def show_grid(self):
        """
        show the lod grid
        """
        next_name = [
            0
        ]  # Wrap next_name in a list to make it mutable from the nested function
        self.names = [
            "Adam",
            "Brian",
            "Cindy",
            "Diana",
            "Evan",
            "Fiona",
            "George",
            "Hannah",
            "Ian",
            "Jack",
            "Kara",
            "Liam",
            "Mona",
            "Nora",
            "Oliver",
            "Pam",
            "Quincy",
            "Rachel",
            "Steve",
            "Tina",
            "Uma",
            "Victor",
            "Wendy",
            "Xavier",
            "Yvonne",
            "Zane",
            "Ashley",
            "Ben",
            "Charlotte",
            "Derek",  # Added more names for a total of 30
        ]

        def gen_name() -> str:
            """
            name generator
            """
            new_name = self.names[next_name[0]]
            next_name[0] += 1  # Increment the index
            if next_name[0] >= len(self.names):
                next_name[0] = (
                    0  # Reset the index if it reaches the end of the names list
                )
            return new_name

        lod = [
            {
                "name": "Alice",
                "age": 18,
                "parent": "David",
                "married": "2023-05-24",
                "weight": 96.48,
                "member": False,
            },
            {
                "name": "Bob",
                "age": 21,
                "parent": "Eve",
                "married": "2023-01-05",
                "weight": 87.85,
                "member": True,
            },
            {
                "name": "Carol",
                "age": 42,
                "parent": "Frank",
                "married": "2007-09-27",
                "weight": 51.81,
                "member": False,
            },
            {
                "name": "Dave",
                "age": 35,
                "parent": "Alice",
                "married": "2019-04-12",
                "weight": 72.28,
                "member": True,
            },
            {
                "name": "Ella",
                "age": 29,
                "parent": "Bob",
                "married": "2013-06-26",
                "weight": 58.09,
                "member": False,
            },
            {
                "name": "Frank",
                "age": 28,
                "parent": "Bob",
                "married": "2027-05-25",
                "weight": 81.32,
                "member": True,
            },
            {
                "name": "Grace",
                "age": 21,
                "parent": "Ella",
                "married": "2023-07-02",
                "weight": 95.36,
                "member": False,
            },
            {
                "name": "Hannah",
                "age": 49,
                "parent": "Frank",
                "married": "1994-01-14",
                "weight": 66.14,
                "member": True,
            },
            {
                "name": "Ian",
                "age": 43,
                "parent": "Bob",
                "married": "2015-05-15",
                "weight": 66.94,
                "member": False,
            },
            {
                "name": "Jill",
                "age": 22,
                "parent": "Carol",
                "married": "2019-06-05",
                "weight": 75.45,
                "member": False,
            },
            {
                "name": "Kevin",
                "age": 39,
                "parent": "Dave",
                "married": "2008-12-09",
                "weight": 95.58,
                "member": True,
            },
            {
                "name": "Liam",
                "age": 46,
                "parent": "Bob",
                "married": "2001-09-15",
                "weight": 86.69,
                "member": True,
            },
            {
                "name": "Mona",
                "age": 31,
                "parent": "Alice",
                "married": "2023-07-01",
                "weight": 88.72,
                "member": False,
            },
        ]

        def show():
            with ui.grid(columns=1) as self.grid_container:
                grid_config = GridConfig(
                    key_col="name",
                    keygen_callback=gen_name,  # Use name generator for new names
                    editable=True,
                    multiselect=True,
                    with_buttons=True,
                    debug=self.args.debug,
                )
                self.lod_grid = ListOfDictsGrid(lod=lod, config=grid_config)
                self.lod_grid.set_checkbox_selection("name")
                self.lod_grid.set_checkbox_renderer("member")

                # setup some grid event listeners
                # https://www.ag-grid.com/javascript-data-grid/grid-events/
                self.lod_grid.ag_grid.on("celled", self.on_cell_clicked)
                self.lod_grid.ag_grid.on(
                    "cellDoubleClicked", self.on_cell_double_clicked
                )
                self.lod_grid.ag_grid.on("rowSelected", self.on_row_selected)
                self.lod_grid.ag_grid.on("selectionChanged", self.on_selection_changed)
                self.lod_grid.ag_grid.on("cellValueChanged", self.on_cell_value_changed)

        await self.setup_content_div(show)

    async def on_cell_clicked(self, event):
        await self.on_grid_event(event, "cellClicked")

    async def on_cell_double_clicked(self, event):
        await self.on_grid_event(event, "cellDoubleClicked")

    async def on_row_selected(self, event):
        await self.on_grid_event(event, "rowSelected")

    async def on_selection_changed(self, event):
        await self.on_grid_event(event, "selectionChanged")

    async def on_cell_value_changed(self, event):
        await self.on_grid_event(event, "cellValueChanged")

    async def on_grid_event(self, event, source):
        """
        React on ag_grid event
        See https://www.ag-grid.com/javascript-data-grid/grid-events/

        """
        args = event.args
        # Custom logic or message formatting can be based on the source
        if source in ["cellDoubleClicked", "cellClicked"]:
            msg = f"{source}: row:  {args['rowId']} column {args['colId']}"
        elif source == "cellValueChanged":
            msg = f"{source}: {args['oldValue']}{args['newValue']} row:  {args['rowId']} column {args['colId']}"
        else:
            msg = f"grid event from {source}: {event.args}"
        # lambda event: ui.notify(f'selected row: {event.args["rowIndex"]}'))
        # lambda event: ui.notify(f'Cell value: {event.args["value"]}'))
        # self.on_property_grid_selection_change
        print(msg)

        ui.notify(msg)

    async def show_dictedit(self):
        """
        show the DictEdit examples
        """
        # Updated sample_dict with a datetime field for enrollment_date
        sample_dict = {
            "given_name": "Alice",
            "family_name": "Wonderland",
            "age": 30,
            "is_student": False,
            "enrollment_date": datetime.now(),  # Set default to current time
        }

        sample_element = Element("hydrogen", "Q556", 1)

        def show():
            customization = {
                "_form_": {"title": "Student", "icon": "person"},
                "given_name": {"label": "Given Name", "size": 50},
                "family_name": {"label": "Family Name", "size": 50},
                "enrollment_date": {
                    "label": "Enrollment Date",
                    "widget": "datetime",
                },  # Customization for datetime
            }
            with ui.grid(columns=3):
                self.dict_edit1 = DictEdit(sample_dict, customization=customization)
                self.dict_edit1.expansion.open()
                self.dict_edit2 = DictEdit(sample_element)
                self.dict_edit2.expansion.open()

        await self.setup_content_div(show)

    async def show_debounce_demo(self):
        """
        Show an input field and search wikipedia
        """

        def show():
            async def perform_search(query: str):
                """Perform a Wikipedia search and display the results."""
                with result_row:
                    result_row.clear()
                    ui.notify(
                        f"Searching... {query}"
                    )  # Notify user that search is happening
                search_instance = WikipediaSearch(
                    country=self.country
                )  # Create an instance of WikipediaSearch
                results = search_instance.search(query, limit=int(self.limit))
                with result_row:
                    result_row.clear()  # Clear the 'Searching...' label or previous results
                    if results:
                        for result in results:
                            link = Link.create(result["url"], result["title"])
                            html = f"{link}:{result['summary']}<br>"
                            ui.html(html)
                    else:
                        ui.label("No results found.")

            async def on_input_change(_event=None):
                await debouncer_ui.debounce(perform_search, search_input.value)

            self.country = "en"
            self.limit = 7
            wikipedias = ["en", "zh", "es", "de", "fr", "ru", "it", "pt", "hi"]
            with ui.row():
                search_input = ui.input("Search Wikipedia:", on_change=on_input_change)
                _country_select = ui.select(
                    wikipedias,
                    label="wikipedia",
                    value=self.country,
                    on_change=on_input_change,
                ).bind_value_to(self, "country")
                _limit_input = (
                    ui.number(
                        label="limit", value=self.limit, on_change=on_input_change
                    )
                    .bind_value_to(self, "limit")
                    .props("size=5")
                )

            result_row = ui.row()  # This will be the container for search results
            debouncer_ui = DebouncerUI(parent=result_row)

        await self.setup_content_div(show)

    async def show_hide_show_demo(self):
        """
        Demonstrate the HideShow project.
        """

        def show():
            hide_show_section = HideShow(("Hide", "Show More"))
            with hide_show_section.content_div:
                ui.label(
                    "This is the hidden content. Click the button to hide/show this text."
                )

        await self.setup_content_div(show)

    async def show_image_cropper(self):
        """
        Demonstrate the ImageCropper class by creating a local sample image.
        """

        def show():
            # Create a temporary file path for the demo
            tmp_dir = tempfile.gettempdir()
            img_filename = "ngdemo_cropper_sample.jpg"
            img_path = os.path.join(tmp_dir, img_filename)

            # Create a generated sample image if it doesn't exist
            # We recreate it every time to 'reset' the demo if previous users cropped it heavily
            img = Image.new("RGB", (600, 400), color=(73, 109, 137))
            d = ImageDraw.Draw(img)
            d.text((20, 20), "NiceGUI Image Cropper Demo", fill=(255, 255, 0))
            d.rectangle([100, 100, 300, 300], outline="white", width=5)
            d.ellipse([350, 150, 550, 350], outline="lightgreen", width=5)
            img.save(img_path)

            # Instructions
            ui.markdown(
                f"**Instructions**: Drag on the image to select an area. Click the checkmark to crop. Use arrows to rotate."
            )
            ui.label(f"Local file: {img_path}").classes("text-xs text-gray-500")

            # Setup Cropper
            with ui.card().classes("w-full") as container:
                cropper = ImageCropper(self)
                cropper.file_path = img_path
                # Note: ui.interactive_image handles local file paths automatically
                cropper.setup_ui(container, img_path)

        await self.setup_content_div(show)

    async def show_log_element_handler_demo(self):
        """
        demonstrate logging via log_view
        """

        def on_loglevel_change(value):
            if self.log_view:
                self.log_view.setLevel(value)

        def show():
            with ui.row():
                ui.select(
                    {
                        logging.NOTSET: "All",
                        logging.DEBUG: f"{LogView.LOG_LEVEL_ICONS[logging.DEBUG]} Debug",
                        logging.INFO: f"{LogView.LOG_LEVEL_ICONS[logging.INFO]} Info",
                        logging.WARNING: f"{LogView.LOG_LEVEL_ICONS[logging.WARNING]} Warning",
                        logging.ERROR: f"{LogView.LOG_LEVEL_ICONS[logging.ERROR]} Error",
                        logging.CRITICAL: f"{LogView.LOG_LEVEL_ICONS[logging.CRITICAL]} Critical",
                    },
                    value=logging.NOTSET,
                    on_change=lambda e: on_loglevel_change(e.value),
                )

            with ui.row():
                ui.button("Debug", on_click=lambda: self.logger.debug("DEBUG"))
                ui.button("Info", on_click=lambda: self.logger.info("INFO"))
                ui.button("Warning", on_click=lambda: self.logger.warning("WARNING"))
                ui.button("Error", on_click=lambda: self.logger.error("ERROR"))
                ui.button("Critical", on_click=lambda: self.logger.critical("CRITICAL"))

        await self.setup_content_div(show)

    async def show_shell_demo(self):
        """
        Demonstrate Shell execution with tee and capture using safe predefined commands.
        """
        # predefined safe shell commands
        commands = {
            "Echo greeting": "echo Hello from shell",
            "List files": "ls -l | head -5",
            "Trigger error": "ls non_existent_file",
            "Combined": "echo Hello from shell && ls -l | head -5 && ls non_existent_file",
        }

        # display a labeled preformatted text block
        def labeled_pre(label: str, value: str, color: str = "black"):
            ui.label(f"{label}:").style("font-weight: bold")
            ui.html(f"<pre>{value}</pre>").style(f"color: {color}")

        # run the selected command and display output
        def run_cmd(cmd_select):
            from basemkit.shell import Shell

            shell = Shell()
            cmd = commands[cmd_select.value]
            result = shell.run(cmd, tee=True)
            labeled_pre("CMD", cmd)
            labeled_pre("STDOUT", result.stdout)
            if result.stderr.strip():
                labeled_pre("STDERR", result.stderr, color="red")

        # setup the shell demo UI
        async def show():
            cmd_select = ui.select(
                list(commands.keys()), label="Choose command", value="Combined"
            ).classes("w-full")
            ui.button("Run", on_click=lambda: run_cmd(cmd_select))

        await self.setup_content_div(show)

    async def show_taskrunner_demo(self):
        """
        Demonstrate the TaskRunner with timeout, progress, and cancellation
        """

        def show():
            # Create progress bar for TaskRunner
            progress_bar = NiceguiProgressbar(total=100, desc="Task", unit="step")
            task_runner = TaskRunner(timeout=8.0, progress=progress_bar)

            async def hint(title: str, done: bool = False, marker: str = "✅"):
                with content:
                    content.clear()
                with content:
                    if not done:
                        progress_bar.progress.visible = False
                        progress_bar.reset()
                        ui.spinner()
                        marker = ""
                    ui.markdown(title + marker)

            async def load_data_5secs():
                title = "Loading data ... for 5 secs"
                await hint(title)

                # Simulate work with progress updates
                for _i in range(50):
                    await asyncio.sleep(0.1)  # 5 seconds total
                    progress_bar.update(2)  # 2% per step

                await hint(title, True)

            async def slow_load_10secs():
                title = "Loading 10 secs (this will timeout after 8 secs)..."
                await hint(title)

                # This will timeout at 8 seconds
                for _i in range(100):
                    await asyncio.sleep(0.1)  # 10 seconds total - will timeout
                    progress_bar.update(1)

                await hint("This should timeout!", True, marker="❌")

            def blocking_task(secs: float = 5):
                time.sleep(secs)
                return f"Blocking task  {secs} secs completed"

            async def blocking_task_5secs():
                title = "Running blocking task for 5 secs"
                await hint(title)
                result = blocking_task(5)
                await hint(result, True)

            async def quick_task_1sec():
                title = "Quick task 1sec"
                await hint(title)

                await asyncio.sleep(1)  # 1 second for quick demo
                await hint(title, True)

            async def cancel_task():
                task_runner.cancel_running()
                task_runner.progress.reset()
                await hint("Task canceled", True, marker="⚠️")

            async def clear():
                await hint("", True, marker="️")
                progress_bar.progress.visible = False

            with ui.card() as content:
                ui.markdown("Ready to run tasks...")

            # Show progress bar
            progress_bar.progress.visible = True

            with ui.row():
                ui.button(
                    "Load Data (5s)", on_click=lambda: task_runner.run(load_data_5secs)
                )
                ui.button(
                    "Quick Task (1s)", on_click=lambda: task_runner.run(quick_task_1sec)
                )
                ui.button(
                    "Blocking Task (5s)",
                    on_click=lambda: task_runner.run(blocking_task_5secs),
                )
                ui.button(
                    "Timeout Test (8s+)",
                    on_click=lambda: task_runner.run(slow_load_10secs),
                )
                ui.button("Cancel", on_click=cancel_task)
                ui.button("Clear", on_click=clear)

            with ui.row():
                status_label = ui.label("")

                # Update status every 100ms using timer
                ui.timer(0.1, lambda: status_label.set_text(task_runner.get_status()))

        await self.setup_content_div(show)

    async def show_tristate_demo(self):
        """
        Demonstrate the Tristate project.
        """

        def on_change():
            ui.notify(
                f"New State: {self.tristate.current_icon_index} ({self.tristate.utf8_icon})"
            )

        def update_icon_set_label(icon_set_name: str):
            # Update the label to show the icons of the new set
            self.icon_set_label.set_text(
                f'Icons in Set {icon_set_name}: {" ".join(Tristate.ICON_SETS[icon_set_name])}'
            )

        def on_icon_set_change(event):
            """
            react on change icon set
            """
            new_icon_set = event.value
            self.tristate.icon_set = Tristate.ICON_SETS[new_icon_set]
            self.tristate.current_icon_index = 0  # Reset to first icon of new set
            self.tristate.update_props()
            update_icon_set_label(new_icon_set)

        def show():
            ui.label("Tristate Demo:")
            # Initialize Tristate component with the default icon set
            default_icon_set_name = "marks"

            # Label to display the icons in the current set
            self.icon_set_label = ui.label()
            update_icon_set_label(default_icon_set_name)

            # Dropdown for selecting the icon set
            icon_set_names = list(Tristate.ICON_SETS.keys())
            self.add_select(
                "Choose Icon Set", icon_set_names, on_change=on_icon_set_change
            )

            ui.label("Click to try:")
            self.tristate = Tristate(
                icon_set_name=default_icon_set_name, on_change=on_change
            )

        await self.setup_content_div(show)

    async def show_colormap_demo(self):
        """
        Display the ColorMap demo interactively
        allowing to set grid size, start and end color
        and luminance and saturation parameters
        """

        def update_grid(color_map, grid_container):
            grid_container.clear()
            with grid_container:
                grid = ui.grid(columns=color_map.num_levels)
                for row in range(color_map.num_levels):
                    for col in range(color_map.num_levels):
                        color = color_map.get_color(row, col)
                        with grid:
                            button = ui.button(color.hex_l, color=f"{color.hex_l}")
                            button.style(f"""
                                width: 50px;
                                height: 50px;
                                font-size: 8px;
                                padding: 2px;
                                """)

        def create_slider(label, min_val, max_val, value, step, on_change):
            ui.label(f"{label}:")
            slider = ui.slider(min=min_val, max=max_val, value=value, step=step).props(
                "label-always"
            )
            slider.on("update:model-value", on_change)
            return slider

        def show():
            color_map1 = ColorMap()

            def refresh_color_map():
                color_map = ColorMap(
                    start_color=start.value,
                    end_color=end.value,
                    num_levels=int(levels.value),
                    lum_min=lum_min.value,
                    lum_max=lum_max.value,
                    sat_f=sat.value,
                )
                update_grid(color_map, grid_container)

            with ui.row():
                with ui.card().classes("w-1/3"):
                    ui.label("ColorMap Demo").classes("text-h4")
                    with ui.row():
                        start = ui.color_input(
                            "Start Color",
                            value=color_map1.start_color.hex_l,
                            on_change=refresh_color_map,
                        )
                        end = ui.color_input(
                            "End Color",
                            value=color_map1.end_color.hex_l,
                            on_change=refresh_color_map,
                        )
                    with ui.grid(columns=2):
                        levels = create_slider("Levels", 2, 10, 5, 1, refresh_color_map)
                        lum_min = create_slider(
                            "Min Luminance",
                            0,
                            1,
                            color_map1.lum_min,
                            0.05,
                            refresh_color_map,
                        )
                        lum_max = create_slider(
                            "Max Luminance",
                            0,
                            1,
                            color_map1.lum_max,
                            0.02,
                            refresh_color_map,
                        )
                        sat = create_slider(
                            "Saturation",
                            0,
                            1,
                            color_map1.sat_f,
                            0.05,
                            refresh_color_map,
                        )
                    ui.button("Refresh", on_click=refresh_color_map)

                grid_container = ui.card().classes("w-1/3")

                refresh_color_map()  # Initial display

        await self.setup_content_div(show)

    async def show_combobox_demo(self):
        """
        Demo to showcase the ComboBox class with both predefined options and user input capability,
        including a button to dynamically change the options using a list of chemical elements.
        """

        def on_combobox_change(event):
            """Handle changes in the ComboBox selection."""
            selected_value = (
                event.sender.value
            )  # Fetching the current value from the combobox
            ui.notify(f"Selected: {selected_value}")

        def update_combobox_options():
            """Randomly modifies the list of chemical elements and updates the ComboBox options."""
            random_size = random.randint(10, len(self.elements))  # Random subset size
            new_elements = random.sample(self.elements, random_size)
            self.element_combobox.update_options(new_elements)
            ui.notify(f"Options have been updated to show {random_size} elements.")

        def show():
            with ui.row():
                # Initial list of chemical elements
                self.elements = [
                    "Hydrogen",
                    "Helium",
                    "Lithium",
                    "Beryllium",
                    "Boron",
                    "Carbon",
                    "Nitrogen",
                    "Oxygen",
                    "Fluorine",
                    "Neon",
                    "Sodium",
                    "Magnesium",
                    "Aluminum",
                    "Silicon",
                    "Phosphorus",
                    "Sulfur",
                    "Chlorine",
                    "Argon",
                    "Potassium",
                    "Calcium",
                    # Extended to cover more elements up to Zinc
                    "Scandium",
                    "Titanium",
                    "Vanadium",
                    "Chromium",
                    "Manganese",
                    "Iron",
                    "Cobalt",
                    "Nickel",
                    "Copper",
                    "Zinc",
                ]
                self.element_combobox = ComboBox(
                    label="Select a Chemical Element",
                    width_chars=35,
                    options=self.elements,
                    clearable=True,
                    new_value_mode="add-unique",
                    on_change=on_combobox_change,
                )

                # Button to update the options in the ComboBox
                ui.button("Update Options", on_click=update_combobox_options)

        await self.setup_content_div(show)

    async def show_gpxviewer(self):
        """
        show gpx demo
        """

        async def load_gpx(url: str):
            viewer = GPXViewer.from_url(url)
            viewer.show()

        async def show():
            """
            show off GPXViewer with some samples
            """

            ui.select(
                options=list(GPXViewer.samples.keys()),
                label="Select GPX Track",
                on_change=lambda e: load_gpx(GPXViewer.samples[e.value]),
            ).classes("w-64")

        await self.setup_content_div(show)

    async def show_leaflet(self):
        """Show leaflet demo with locations and path drawing"""

        async def show():

            self.locations = {
                "London": (51.505, -0.090),
                "Berlin": (52.5200, 13.4049),
                "Hannover": (52.37487, 9.74168),
                "New York": (40.7306, -74.0060),
                "Beijing": (39.9042, 116.4074),
                "Tokyo": (35.6895, 139.6917),
            }
            self.zoom_level = 9
            london = self.locations["London"]

            self.map = LeafletMap(
                center=london, zoom=self.zoom_level, classes="w-full h-96"
            )
            await self.map.initialized()

            with ui.row() as self.select_row:
                ui.select(
                    options=list(self.locations.keys()),  # Use city names
                    label="Location",
                    value="London",
                    on_change=lambda e: setattr(
                        self.map, "center", self.locations[e.value]
                    ),
                ).classes("w-40")
                ui.select(
                    {i: i for i in range(9, 18)},
                    label="Zoom",
                    value=self.zoom_level,
                    on_change=lambda e: setattr(self.map, "zoom", e.value),
                ).classes("w-40")
                ui.label().bind_text_from(
                    self.map,
                    "center",
                    lambda center: f"Center: {center[0]:.3f}, {center[1]:.3f}",
                )
                ui.label().bind_text_from(
                    self.map, "zoom", lambda zoom: f"Zoom: {zoom}"
                )
                # ui.label().bind_text(lambda: f'Layers: {len(self.map.layers)}')  # Layer counter

            with ui.grid(columns=2) as self.grid:
                ui.button(
                    icon="zoom_in",
                    on_click=lambda: setattr(self.map, "zoom", self.map.zoom + 1),
                )
                ui.button(
                    icon="zoom_out",
                    on_click=lambda: setattr(self.map, "zoom", self.map.zoom - 1),
                )
                # ui.button('Fit world', on_click=lambda: self.map.run_map_method('fitWorld'))
                ui.button("Clear layers", on_click=lambda: self.map.clear_layers())

        await self.setup_content_div(show)

    def configure_menu(self):
        """
        configure my menu
        """
        InputWebSolution.configure_menu(self)
        self.sso_solution = SsoSolution(webserver=self.webserver)
        self.sso_solution.configure_menu()

    async def home(self):
        """
        provide the main content page
        """

        def setup_home():
            # Define the links and labels in a dictionary
            links = {
                "nicegui solutions bazaar": "/solutions",
                "ColorMap Demo": "/colormap",
                "ColorSchema": "/color_schema",
                "ComboBox Demo": "/combobox",
                "Debounce Demo": "/stars",
                "DictEdit": "/dictedit",
                "GPX-Viewer": "/gpxviewer",
                "HideShow Demo": "/hideshow",
                "Image Cropper": "/image_cropper",
                "Lang": "/langs",
                "Leaflet Map": "/leaflet",
                "ListOfDictsGrid": "/grid",
                "Log Element Handler Demo": "/log_handler",
                "TaskRunner Demo": "/taskrunner",
                "Tristate Demo": "/tristate",
                "pdfviewer": "/pdfviewer",
                "Progressbar": "/progress",
                "Shell Demo": "/shell",
            }

            # Generate the HTML using the dictionary
            html_content = "<ul>"
            for label, link in links.items():
                html_content += f'<li><a href="{link}">{label}</a></li>'
            html_content += "</ul>"

            # html_content now contains the HTML code to render the list of links
            ui.html(html_content)

        await self.setup_content_div(setup_home)

__init__(webserver, client)

Initialize the NiceGuiWidgetsDemoContext.

Calls the constructor of the base class ClientWebContext to ensure proper initialization and then performs any additional setup specific to NiceGuiWidgetsDemoContext.

Parameters:

Name Type Description Default
webserver NiceGuiWebserver

The webserver instance associated with this context.

required
client Client

The client instance this context is associated with.

required
Source code in ngwidgets/widgets_demo.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def __init__(self, webserver: "NiceGuiWidgetsDemoWebserver", client: Client):
    """
    Initialize the NiceGuiWidgetsDemoContext.

    Calls the constructor of the base class ClientWebContext to ensure proper initialization
    and then performs any additional setup specific to NiceGuiWidgetsDemoContext.

    Args:
        webserver (NiceGuiWebserver): The webserver instance associated with this context.
        client (Client): The client instance this context is associated with.
    """
    super().__init__(webserver, client)  # Call to the superclass constructor
    self.projects = self.webserver.projects
    # pdf_url = "https://www.africau.edu/images/default/sample.pdf"
    self.pdf_url = "https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf"

configure_menu()

configure my menu

Source code in ngwidgets/widgets_demo.py
1020
1021
1022
1023
1024
1025
1026
def configure_menu(self):
    """
    configure my menu
    """
    InputWebSolution.configure_menu(self)
    self.sso_solution = SsoSolution(webserver=self.webserver)
    self.sso_solution.configure_menu()

home() async

provide the main content page

Source code in ngwidgets/widgets_demo.py
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
async def home(self):
    """
    provide the main content page
    """

    def setup_home():
        # Define the links and labels in a dictionary
        links = {
            "nicegui solutions bazaar": "/solutions",
            "ColorMap Demo": "/colormap",
            "ColorSchema": "/color_schema",
            "ComboBox Demo": "/combobox",
            "Debounce Demo": "/stars",
            "DictEdit": "/dictedit",
            "GPX-Viewer": "/gpxviewer",
            "HideShow Demo": "/hideshow",
            "Image Cropper": "/image_cropper",
            "Lang": "/langs",
            "Leaflet Map": "/leaflet",
            "ListOfDictsGrid": "/grid",
            "Log Element Handler Demo": "/log_handler",
            "TaskRunner Demo": "/taskrunner",
            "Tristate Demo": "/tristate",
            "pdfviewer": "/pdfviewer",
            "Progressbar": "/progress",
            "Shell Demo": "/shell",
        }

        # Generate the HTML using the dictionary
        html_content = "<ul>"
        for label, link in links.items():
            html_content += f'<li><a href="{link}">{label}</a></li>'
        html_content += "</ul>"

        # html_content now contains the HTML code to render the list of links
        ui.html(html_content)

    await self.setup_content_div(setup_home)

on_grid_event(event, source) async

React on ag_grid event See https://www.ag-grid.com/javascript-data-grid/grid-events/

Source code in ngwidgets/widgets_demo.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
async def on_grid_event(self, event, source):
    """
    React on ag_grid event
    See https://www.ag-grid.com/javascript-data-grid/grid-events/

    """
    args = event.args
    # Custom logic or message formatting can be based on the source
    if source in ["cellDoubleClicked", "cellClicked"]:
        msg = f"{source}: row:  {args['rowId']} column {args['colId']}"
    elif source == "cellValueChanged":
        msg = f"{source}: {args['oldValue']}{args['newValue']} row:  {args['rowId']} column {args['colId']}"
    else:
        msg = f"grid event from {source}: {event.args}"
    # lambda event: ui.notify(f'selected row: {event.args["rowIndex"]}'))
    # lambda event: ui.notify(f'Cell value: {event.args["value"]}'))
    # self.on_property_grid_selection_change
    print(msg)

    ui.notify(msg)

show_colormap_demo() async

Display the ColorMap demo interactively allowing to set grid size, start and end color and luminance and saturation parameters

Source code in ngwidgets/widgets_demo.py
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
async def show_colormap_demo(self):
    """
    Display the ColorMap demo interactively
    allowing to set grid size, start and end color
    and luminance and saturation parameters
    """

    def update_grid(color_map, grid_container):
        grid_container.clear()
        with grid_container:
            grid = ui.grid(columns=color_map.num_levels)
            for row in range(color_map.num_levels):
                for col in range(color_map.num_levels):
                    color = color_map.get_color(row, col)
                    with grid:
                        button = ui.button(color.hex_l, color=f"{color.hex_l}")
                        button.style(f"""
                            width: 50px;
                            height: 50px;
                            font-size: 8px;
                            padding: 2px;
                            """)

    def create_slider(label, min_val, max_val, value, step, on_change):
        ui.label(f"{label}:")
        slider = ui.slider(min=min_val, max=max_val, value=value, step=step).props(
            "label-always"
        )
        slider.on("update:model-value", on_change)
        return slider

    def show():
        color_map1 = ColorMap()

        def refresh_color_map():
            color_map = ColorMap(
                start_color=start.value,
                end_color=end.value,
                num_levels=int(levels.value),
                lum_min=lum_min.value,
                lum_max=lum_max.value,
                sat_f=sat.value,
            )
            update_grid(color_map, grid_container)

        with ui.row():
            with ui.card().classes("w-1/3"):
                ui.label("ColorMap Demo").classes("text-h4")
                with ui.row():
                    start = ui.color_input(
                        "Start Color",
                        value=color_map1.start_color.hex_l,
                        on_change=refresh_color_map,
                    )
                    end = ui.color_input(
                        "End Color",
                        value=color_map1.end_color.hex_l,
                        on_change=refresh_color_map,
                    )
                with ui.grid(columns=2):
                    levels = create_slider("Levels", 2, 10, 5, 1, refresh_color_map)
                    lum_min = create_slider(
                        "Min Luminance",
                        0,
                        1,
                        color_map1.lum_min,
                        0.05,
                        refresh_color_map,
                    )
                    lum_max = create_slider(
                        "Max Luminance",
                        0,
                        1,
                        color_map1.lum_max,
                        0.02,
                        refresh_color_map,
                    )
                    sat = create_slider(
                        "Saturation",
                        0,
                        1,
                        color_map1.sat_f,
                        0.05,
                        refresh_color_map,
                    )
                ui.button("Refresh", on_click=refresh_color_map)

            grid_container = ui.card().classes("w-1/3")

            refresh_color_map()  # Initial display

    await self.setup_content_div(show)

show_combobox_demo() async

Demo to showcase the ComboBox class with both predefined options and user input capability, including a button to dynamically change the options using a list of chemical elements.

Source code in ngwidgets/widgets_demo.py
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
async def show_combobox_demo(self):
    """
    Demo to showcase the ComboBox class with both predefined options and user input capability,
    including a button to dynamically change the options using a list of chemical elements.
    """

    def on_combobox_change(event):
        """Handle changes in the ComboBox selection."""
        selected_value = (
            event.sender.value
        )  # Fetching the current value from the combobox
        ui.notify(f"Selected: {selected_value}")

    def update_combobox_options():
        """Randomly modifies the list of chemical elements and updates the ComboBox options."""
        random_size = random.randint(10, len(self.elements))  # Random subset size
        new_elements = random.sample(self.elements, random_size)
        self.element_combobox.update_options(new_elements)
        ui.notify(f"Options have been updated to show {random_size} elements.")

    def show():
        with ui.row():
            # Initial list of chemical elements
            self.elements = [
                "Hydrogen",
                "Helium",
                "Lithium",
                "Beryllium",
                "Boron",
                "Carbon",
                "Nitrogen",
                "Oxygen",
                "Fluorine",
                "Neon",
                "Sodium",
                "Magnesium",
                "Aluminum",
                "Silicon",
                "Phosphorus",
                "Sulfur",
                "Chlorine",
                "Argon",
                "Potassium",
                "Calcium",
                # Extended to cover more elements up to Zinc
                "Scandium",
                "Titanium",
                "Vanadium",
                "Chromium",
                "Manganese",
                "Iron",
                "Cobalt",
                "Nickel",
                "Copper",
                "Zinc",
            ]
            self.element_combobox = ComboBox(
                label="Select a Chemical Element",
                width_chars=35,
                options=self.elements,
                clearable=True,
                new_value_mode="add-unique",
                on_change=on_combobox_change,
            )

            # Button to update the options in the ComboBox
            ui.button("Update Options", on_click=update_combobox_options)

    await self.setup_content_div(show)

show_debounce_demo() async

Show an input field and search wikipedia

Source code in ngwidgets/widgets_demo.py
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
async def show_debounce_demo(self):
    """
    Show an input field and search wikipedia
    """

    def show():
        async def perform_search(query: str):
            """Perform a Wikipedia search and display the results."""
            with result_row:
                result_row.clear()
                ui.notify(
                    f"Searching... {query}"
                )  # Notify user that search is happening
            search_instance = WikipediaSearch(
                country=self.country
            )  # Create an instance of WikipediaSearch
            results = search_instance.search(query, limit=int(self.limit))
            with result_row:
                result_row.clear()  # Clear the 'Searching...' label or previous results
                if results:
                    for result in results:
                        link = Link.create(result["url"], result["title"])
                        html = f"{link}:{result['summary']}<br>"
                        ui.html(html)
                else:
                    ui.label("No results found.")

        async def on_input_change(_event=None):
            await debouncer_ui.debounce(perform_search, search_input.value)

        self.country = "en"
        self.limit = 7
        wikipedias = ["en", "zh", "es", "de", "fr", "ru", "it", "pt", "hi"]
        with ui.row():
            search_input = ui.input("Search Wikipedia:", on_change=on_input_change)
            _country_select = ui.select(
                wikipedias,
                label="wikipedia",
                value=self.country,
                on_change=on_input_change,
            ).bind_value_to(self, "country")
            _limit_input = (
                ui.number(
                    label="limit", value=self.limit, on_change=on_input_change
                )
                .bind_value_to(self, "limit")
                .props("size=5")
            )

        result_row = ui.row()  # This will be the container for search results
        debouncer_ui = DebouncerUI(parent=result_row)

    await self.setup_content_div(show)

show_dictedit() async

show the DictEdit examples

Source code in ngwidgets/widgets_demo.py
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
async def show_dictedit(self):
    """
    show the DictEdit examples
    """
    # Updated sample_dict with a datetime field for enrollment_date
    sample_dict = {
        "given_name": "Alice",
        "family_name": "Wonderland",
        "age": 30,
        "is_student": False,
        "enrollment_date": datetime.now(),  # Set default to current time
    }

    sample_element = Element("hydrogen", "Q556", 1)

    def show():
        customization = {
            "_form_": {"title": "Student", "icon": "person"},
            "given_name": {"label": "Given Name", "size": 50},
            "family_name": {"label": "Family Name", "size": 50},
            "enrollment_date": {
                "label": "Enrollment Date",
                "widget": "datetime",
            },  # Customization for datetime
        }
        with ui.grid(columns=3):
            self.dict_edit1 = DictEdit(sample_dict, customization=customization)
            self.dict_edit1.expansion.open()
            self.dict_edit2 = DictEdit(sample_element)
            self.dict_edit2.expansion.open()

    await self.setup_content_div(show)

show_gpxviewer() async

show gpx demo

Source code in ngwidgets/widgets_demo.py
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
async def show_gpxviewer(self):
    """
    show gpx demo
    """

    async def load_gpx(url: str):
        viewer = GPXViewer.from_url(url)
        viewer.show()

    async def show():
        """
        show off GPXViewer with some samples
        """

        ui.select(
            options=list(GPXViewer.samples.keys()),
            label="Select GPX Track",
            on_change=lambda e: load_gpx(GPXViewer.samples[e.value]),
        ).classes("w-64")

    await self.setup_content_div(show)

show_grid() async

show the lod grid

Source code in ngwidgets/widgets_demo.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
async def show_grid(self):
    """
    show the lod grid
    """
    next_name = [
        0
    ]  # Wrap next_name in a list to make it mutable from the nested function
    self.names = [
        "Adam",
        "Brian",
        "Cindy",
        "Diana",
        "Evan",
        "Fiona",
        "George",
        "Hannah",
        "Ian",
        "Jack",
        "Kara",
        "Liam",
        "Mona",
        "Nora",
        "Oliver",
        "Pam",
        "Quincy",
        "Rachel",
        "Steve",
        "Tina",
        "Uma",
        "Victor",
        "Wendy",
        "Xavier",
        "Yvonne",
        "Zane",
        "Ashley",
        "Ben",
        "Charlotte",
        "Derek",  # Added more names for a total of 30
    ]

    def gen_name() -> str:
        """
        name generator
        """
        new_name = self.names[next_name[0]]
        next_name[0] += 1  # Increment the index
        if next_name[0] >= len(self.names):
            next_name[0] = (
                0  # Reset the index if it reaches the end of the names list
            )
        return new_name

    lod = [
        {
            "name": "Alice",
            "age": 18,
            "parent": "David",
            "married": "2023-05-24",
            "weight": 96.48,
            "member": False,
        },
        {
            "name": "Bob",
            "age": 21,
            "parent": "Eve",
            "married": "2023-01-05",
            "weight": 87.85,
            "member": True,
        },
        {
            "name": "Carol",
            "age": 42,
            "parent": "Frank",
            "married": "2007-09-27",
            "weight": 51.81,
            "member": False,
        },
        {
            "name": "Dave",
            "age": 35,
            "parent": "Alice",
            "married": "2019-04-12",
            "weight": 72.28,
            "member": True,
        },
        {
            "name": "Ella",
            "age": 29,
            "parent": "Bob",
            "married": "2013-06-26",
            "weight": 58.09,
            "member": False,
        },
        {
            "name": "Frank",
            "age": 28,
            "parent": "Bob",
            "married": "2027-05-25",
            "weight": 81.32,
            "member": True,
        },
        {
            "name": "Grace",
            "age": 21,
            "parent": "Ella",
            "married": "2023-07-02",
            "weight": 95.36,
            "member": False,
        },
        {
            "name": "Hannah",
            "age": 49,
            "parent": "Frank",
            "married": "1994-01-14",
            "weight": 66.14,
            "member": True,
        },
        {
            "name": "Ian",
            "age": 43,
            "parent": "Bob",
            "married": "2015-05-15",
            "weight": 66.94,
            "member": False,
        },
        {
            "name": "Jill",
            "age": 22,
            "parent": "Carol",
            "married": "2019-06-05",
            "weight": 75.45,
            "member": False,
        },
        {
            "name": "Kevin",
            "age": 39,
            "parent": "Dave",
            "married": "2008-12-09",
            "weight": 95.58,
            "member": True,
        },
        {
            "name": "Liam",
            "age": 46,
            "parent": "Bob",
            "married": "2001-09-15",
            "weight": 86.69,
            "member": True,
        },
        {
            "name": "Mona",
            "age": 31,
            "parent": "Alice",
            "married": "2023-07-01",
            "weight": 88.72,
            "member": False,
        },
    ]

    def show():
        with ui.grid(columns=1) as self.grid_container:
            grid_config = GridConfig(
                key_col="name",
                keygen_callback=gen_name,  # Use name generator for new names
                editable=True,
                multiselect=True,
                with_buttons=True,
                debug=self.args.debug,
            )
            self.lod_grid = ListOfDictsGrid(lod=lod, config=grid_config)
            self.lod_grid.set_checkbox_selection("name")
            self.lod_grid.set_checkbox_renderer("member")

            # setup some grid event listeners
            # https://www.ag-grid.com/javascript-data-grid/grid-events/
            self.lod_grid.ag_grid.on("celled", self.on_cell_clicked)
            self.lod_grid.ag_grid.on(
                "cellDoubleClicked", self.on_cell_double_clicked
            )
            self.lod_grid.ag_grid.on("rowSelected", self.on_row_selected)
            self.lod_grid.ag_grid.on("selectionChanged", self.on_selection_changed)
            self.lod_grid.ag_grid.on("cellValueChanged", self.on_cell_value_changed)

    await self.setup_content_div(show)

show_hide_show_demo() async

Demonstrate the HideShow project.

Source code in ngwidgets/widgets_demo.py
506
507
508
509
510
511
512
513
514
515
516
517
518
async def show_hide_show_demo(self):
    """
    Demonstrate the HideShow project.
    """

    def show():
        hide_show_section = HideShow(("Hide", "Show More"))
        with hide_show_section.content_div:
            ui.label(
                "This is the hidden content. Click the button to hide/show this text."
            )

    await self.setup_content_div(show)

show_image_cropper() async

Demonstrate the ImageCropper class by creating a local sample image.

Source code in ngwidgets/widgets_demo.py
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
async def show_image_cropper(self):
    """
    Demonstrate the ImageCropper class by creating a local sample image.
    """

    def show():
        # Create a temporary file path for the demo
        tmp_dir = tempfile.gettempdir()
        img_filename = "ngdemo_cropper_sample.jpg"
        img_path = os.path.join(tmp_dir, img_filename)

        # Create a generated sample image if it doesn't exist
        # We recreate it every time to 'reset' the demo if previous users cropped it heavily
        img = Image.new("RGB", (600, 400), color=(73, 109, 137))
        d = ImageDraw.Draw(img)
        d.text((20, 20), "NiceGUI Image Cropper Demo", fill=(255, 255, 0))
        d.rectangle([100, 100, 300, 300], outline="white", width=5)
        d.ellipse([350, 150, 550, 350], outline="lightgreen", width=5)
        img.save(img_path)

        # Instructions
        ui.markdown(
            f"**Instructions**: Drag on the image to select an area. Click the checkmark to crop. Use arrows to rotate."
        )
        ui.label(f"Local file: {img_path}").classes("text-xs text-gray-500")

        # Setup Cropper
        with ui.card().classes("w-full") as container:
            cropper = ImageCropper(self)
            cropper.file_path = img_path
            # Note: ui.interactive_image handles local file paths automatically
            cropper.setup_ui(container, img_path)

    await self.setup_content_div(show)

show_issue_1786() async

https://github.com/zauberzeug/nicegui/discussions/1786

Source code in ngwidgets/widgets_demo.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
async def show_issue_1786(self):
    """
    https://github.com/zauberzeug/nicegui/discussions/1786
    """

    def foreground_no_slot(event):
        ui.notify(f"{event.sender.text} ed")

    async def background_no_slot(event):
        await run.io_bound(foreground_no_slot, event)

    def foreground_with_slot(event):
        with self.button_row:
            ui.notify(f"{event.sender.text} ed")

    async def background_with_slot(event):
        await run.io_bound(foreground_with_slot, event)

    def show():
        with ui.row() as self.button_row:
            ui.button("foreground no slot", on_=foreground_no_slot)
            ui.button("background no slot", on_=background_no_slot)
            ui.button("foreground with slot", on_=foreground_with_slot)
            ui.button("background with slot", on_=background_with_slot)

    await self.setup_content_div(show)

show_langs() async

show languages selection

Source code in ngwidgets/widgets_demo.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
async def show_langs(self):
    """
    show languages selection
    """

    def show():
        # Default language set to English
        default_lang = "en"

        # Get available languages
        languages = Lang.get_language_dict()
        with ui.card().style("width: 12%"):
            with ui.row():
                ui.label("Lang code:")
                # Create a label to display the chosen language with the default language
                lang_label = ui.label(default_lang)
            with ui.row():
                ui.label("Select:")
                # Create a dropdown for language selection with the default language selected
                # Bind the label text to the selection's value, so it updates automatically
                ui.select(languages, value=default_lang).bind_value(
                    lang_label, "text"
                )

    await self.setup_content_div(show)

show_leaflet() async

Show leaflet demo with locations and path drawing

Source code in ngwidgets/widgets_demo.py
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
async def show_leaflet(self):
    """Show leaflet demo with locations and path drawing"""

    async def show():

        self.locations = {
            "London": (51.505, -0.090),
            "Berlin": (52.5200, 13.4049),
            "Hannover": (52.37487, 9.74168),
            "New York": (40.7306, -74.0060),
            "Beijing": (39.9042, 116.4074),
            "Tokyo": (35.6895, 139.6917),
        }
        self.zoom_level = 9
        london = self.locations["London"]

        self.map = LeafletMap(
            center=london, zoom=self.zoom_level, classes="w-full h-96"
        )
        await self.map.initialized()

        with ui.row() as self.select_row:
            ui.select(
                options=list(self.locations.keys()),  # Use city names
                label="Location",
                value="London",
                on_change=lambda e: setattr(
                    self.map, "center", self.locations[e.value]
                ),
            ).classes("w-40")
            ui.select(
                {i: i for i in range(9, 18)},
                label="Zoom",
                value=self.zoom_level,
                on_change=lambda e: setattr(self.map, "zoom", e.value),
            ).classes("w-40")
            ui.label().bind_text_from(
                self.map,
                "center",
                lambda center: f"Center: {center[0]:.3f}, {center[1]:.3f}",
            )
            ui.label().bind_text_from(
                self.map, "zoom", lambda zoom: f"Zoom: {zoom}"
            )
            # ui.label().bind_text(lambda: f'Layers: {len(self.map.layers)}')  # Layer counter

        with ui.grid(columns=2) as self.grid:
            ui.button(
                icon="zoom_in",
                on_click=lambda: setattr(self.map, "zoom", self.map.zoom + 1),
            )
            ui.button(
                icon="zoom_out",
                on_click=lambda: setattr(self.map, "zoom", self.map.zoom - 1),
            )
            # ui.button('Fit world', on_click=lambda: self.map.run_map_method('fitWorld'))
            ui.button("Clear layers", on_click=lambda: self.map.clear_layers())

    await self.setup_content_div(show)

show_log_element_handler_demo() async

demonstrate logging via log_view

Source code in ngwidgets/widgets_demo.py
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
async def show_log_element_handler_demo(self):
    """
    demonstrate logging via log_view
    """

    def on_loglevel_change(value):
        if self.log_view:
            self.log_view.setLevel(value)

    def show():
        with ui.row():
            ui.select(
                {
                    logging.NOTSET: "All",
                    logging.DEBUG: f"{LogView.LOG_LEVEL_ICONS[logging.DEBUG]} Debug",
                    logging.INFO: f"{LogView.LOG_LEVEL_ICONS[logging.INFO]} Info",
                    logging.WARNING: f"{LogView.LOG_LEVEL_ICONS[logging.WARNING]} Warning",
                    logging.ERROR: f"{LogView.LOG_LEVEL_ICONS[logging.ERROR]} Error",
                    logging.CRITICAL: f"{LogView.LOG_LEVEL_ICONS[logging.CRITICAL]} Critical",
                },
                value=logging.NOTSET,
                on_change=lambda e: on_loglevel_change(e.value),
            )

        with ui.row():
            ui.button("Debug", on_click=lambda: self.logger.debug("DEBUG"))
            ui.button("Info", on_click=lambda: self.logger.info("INFO"))
            ui.button("Warning", on_click=lambda: self.logger.warning("WARNING"))
            ui.button("Error", on_click=lambda: self.logger.error("ERROR"))
            ui.button("Critical", on_click=lambda: self.logger.critical("CRITICAL"))

    await self.setup_content_div(show)

show_shell_demo() async

Demonstrate Shell execution with tee and capture using safe predefined commands.

Source code in ngwidgets/widgets_demo.py
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
async def show_shell_demo(self):
    """
    Demonstrate Shell execution with tee and capture using safe predefined commands.
    """
    # predefined safe shell commands
    commands = {
        "Echo greeting": "echo Hello from shell",
        "List files": "ls -l | head -5",
        "Trigger error": "ls non_existent_file",
        "Combined": "echo Hello from shell && ls -l | head -5 && ls non_existent_file",
    }

    # display a labeled preformatted text block
    def labeled_pre(label: str, value: str, color: str = "black"):
        ui.label(f"{label}:").style("font-weight: bold")
        ui.html(f"<pre>{value}</pre>").style(f"color: {color}")

    # run the selected command and display output
    def run_cmd(cmd_select):
        from basemkit.shell import Shell

        shell = Shell()
        cmd = commands[cmd_select.value]
        result = shell.run(cmd, tee=True)
        labeled_pre("CMD", cmd)
        labeled_pre("STDOUT", result.stdout)
        if result.stderr.strip():
            labeled_pre("STDERR", result.stderr, color="red")

    # setup the shell demo UI
    async def show():
        cmd_select = ui.select(
            list(commands.keys()), label="Choose command", value="Combined"
        ).classes("w-full")
        ui.button("Run", on_click=lambda: run_cmd(cmd_select))

    await self.setup_content_div(show)

show_taskrunner_demo() async

Demonstrate the TaskRunner with timeout, progress, and cancellation

Source code in ngwidgets/widgets_demo.py
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
async def show_taskrunner_demo(self):
    """
    Demonstrate the TaskRunner with timeout, progress, and cancellation
    """

    def show():
        # Create progress bar for TaskRunner
        progress_bar = NiceguiProgressbar(total=100, desc="Task", unit="step")
        task_runner = TaskRunner(timeout=8.0, progress=progress_bar)

        async def hint(title: str, done: bool = False, marker: str = "✅"):
            with content:
                content.clear()
            with content:
                if not done:
                    progress_bar.progress.visible = False
                    progress_bar.reset()
                    ui.spinner()
                    marker = ""
                ui.markdown(title + marker)

        async def load_data_5secs():
            title = "Loading data ... for 5 secs"
            await hint(title)

            # Simulate work with progress updates
            for _i in range(50):
                await asyncio.sleep(0.1)  # 5 seconds total
                progress_bar.update(2)  # 2% per step

            await hint(title, True)

        async def slow_load_10secs():
            title = "Loading 10 secs (this will timeout after 8 secs)..."
            await hint(title)

            # This will timeout at 8 seconds
            for _i in range(100):
                await asyncio.sleep(0.1)  # 10 seconds total - will timeout
                progress_bar.update(1)

            await hint("This should timeout!", True, marker="❌")

        def blocking_task(secs: float = 5):
            time.sleep(secs)
            return f"Blocking task  {secs} secs completed"

        async def blocking_task_5secs():
            title = "Running blocking task for 5 secs"
            await hint(title)
            result = blocking_task(5)
            await hint(result, True)

        async def quick_task_1sec():
            title = "Quick task 1sec"
            await hint(title)

            await asyncio.sleep(1)  # 1 second for quick demo
            await hint(title, True)

        async def cancel_task():
            task_runner.cancel_running()
            task_runner.progress.reset()
            await hint("Task canceled", True, marker="⚠️")

        async def clear():
            await hint("", True, marker="️")
            progress_bar.progress.visible = False

        with ui.card() as content:
            ui.markdown("Ready to run tasks...")

        # Show progress bar
        progress_bar.progress.visible = True

        with ui.row():
            ui.button(
                "Load Data (5s)", on_click=lambda: task_runner.run(load_data_5secs)
            )
            ui.button(
                "Quick Task (1s)", on_click=lambda: task_runner.run(quick_task_1sec)
            )
            ui.button(
                "Blocking Task (5s)",
                on_click=lambda: task_runner.run(blocking_task_5secs),
            )
            ui.button(
                "Timeout Test (8s+)",
                on_click=lambda: task_runner.run(slow_load_10secs),
            )
            ui.button("Cancel", on_click=cancel_task)
            ui.button("Clear", on_click=clear)

        with ui.row():
            status_label = ui.label("")

            # Update status every 100ms using timer
            ui.timer(0.1, lambda: status_label.set_text(task_runner.get_status()))

    await self.setup_content_div(show)

show_tristate_demo() async

Demonstrate the Tristate project.

Source code in ngwidgets/widgets_demo.py
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
async def show_tristate_demo(self):
    """
    Demonstrate the Tristate project.
    """

    def on_change():
        ui.notify(
            f"New State: {self.tristate.current_icon_index} ({self.tristate.utf8_icon})"
        )

    def update_icon_set_label(icon_set_name: str):
        # Update the label to show the icons of the new set
        self.icon_set_label.set_text(
            f'Icons in Set {icon_set_name}: {" ".join(Tristate.ICON_SETS[icon_set_name])}'
        )

    def on_icon_set_change(event):
        """
        react on change icon set
        """
        new_icon_set = event.value
        self.tristate.icon_set = Tristate.ICON_SETS[new_icon_set]
        self.tristate.current_icon_index = 0  # Reset to first icon of new set
        self.tristate.update_props()
        update_icon_set_label(new_icon_set)

    def show():
        ui.label("Tristate Demo:")
        # Initialize Tristate component with the default icon set
        default_icon_set_name = "marks"

        # Label to display the icons in the current set
        self.icon_set_label = ui.label()
        update_icon_set_label(default_icon_set_name)

        # Dropdown for selecting the icon set
        icon_set_names = list(Tristate.ICON_SETS.keys())
        self.add_select(
            "Choose Icon Set", icon_set_names, on_change=on_icon_set_change
        )

        ui.label("Click to try:")
        self.tristate = Tristate(
            icon_set_name=default_icon_set_name, on_change=on_change
        )

    await self.setup_content_div(show)

NiceGuiWidgetsDemoWebserver

Bases: InputWebserver

webserver to demonstrate ngwidgets capabilities

Source code in ngwidgets/widgets_demo.py
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
class NiceGuiWidgetsDemoWebserver(InputWebserver):
    """
    webserver to demonstrate ngwidgets capabilities
    """

    @classmethod
    def get_config(cls) -> WebserverConfig:
        copy_right = "(c)2023-2025 Wolfgang Fahl"
        config = WebserverConfig(
            short_name="ngdemo",
            timeout=6.0,
            copy_right=copy_right,
            version=Version(),
            default_port=9856,
        )
        server_config = WebserverConfig.get(config)
        server_config.solution_class = NiceGuiWidgetsDemo
        return server_config

    def __init__(self):
        """
        Constructor
        """
        InputWebserver.__init__(self, config=NiceGuiWidgetsDemoWebserver.get_config())
        self.projects = Projects(topic="nicegui")
        self.projects.load()
        pass

        @app.get("/solutions.yaml")
        def get_solutions_yaml():
            yaml_data = self.projects.to_yaml()
            return Response(content=yaml_data, media_type="text/yaml")

        @ui.page("/solutions")
        async def show_solutions(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_solutions)

        @ui.page("/colormap")
        async def show_colormap_demo(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_colormap_demo)

        @ui.page("/combobox")
        async def show_combobox_demo(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_combobox_demo)

        @ui.page("/components/{solution_id}")
        async def show_components(solution_id: str, client: Client):
            return await self.page(
                client, NiceGuiWidgetsDemo.show_components, solution_id
            )

        @ui.page("/progress")
        async def show_progress(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_progress)

        @ui.page("/shell")
        async def show_shell_demo(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_shell_demo)

        @ui.page("/gpxviewer")
        async def show_gpxviewer(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_gpxviewer)

        @ui.page("/image_cropper")
        async def show_image_cropper(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_image_cropper)

        @ui.page("/langs")
        async def show_langs(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_langs)

        @ui.page("/leaflet")
        async def show_leaflet(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_leaflet)

        @ui.page("/log_handler")
        async def show_log_handler_demo(client: Client):
            return await self.page(
                client, NiceGuiWidgetsDemo.show_log_element_handler_demo
            )

        @ui.page("/color_schema")
        async def show_color_schema(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_color_schema)

        @ui.page("/dictedit")
        async def show_dictedit(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_dictedit)

        @ui.page("/grid")
        async def show_grid(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_grid)

        @ui.page("/hideshow")
        async def show_hide_show(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_hide_show_demo)

        @ui.page("/taskrunner")
        async def show_taskrunner_demo(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_taskrunner_demo)

        @ui.page("/tristate")
        async def show_tristate_demo(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_tristate_demo)

        @ui.page("/pdfviewer")
        async def show_pdf_viewer(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_pdf_viewer)

        @ui.page("/issue1786")
        async def show_issue_1786(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_issue_1786)

        @ui.page("/stars")
        async def show_debounce_demo(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_debounce_demo)

__init__()

Constructor

Source code in ngwidgets/widgets_demo.py
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
def __init__(self):
    """
    Constructor
    """
    InputWebserver.__init__(self, config=NiceGuiWidgetsDemoWebserver.get_config())
    self.projects = Projects(topic="nicegui")
    self.projects.load()
    pass

    @app.get("/solutions.yaml")
    def get_solutions_yaml():
        yaml_data = self.projects.to_yaml()
        return Response(content=yaml_data, media_type="text/yaml")

    @ui.page("/solutions")
    async def show_solutions(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_solutions)

    @ui.page("/colormap")
    async def show_colormap_demo(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_colormap_demo)

    @ui.page("/combobox")
    async def show_combobox_demo(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_combobox_demo)

    @ui.page("/components/{solution_id}")
    async def show_components(solution_id: str, client: Client):
        return await self.page(
            client, NiceGuiWidgetsDemo.show_components, solution_id
        )

    @ui.page("/progress")
    async def show_progress(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_progress)

    @ui.page("/shell")
    async def show_shell_demo(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_shell_demo)

    @ui.page("/gpxviewer")
    async def show_gpxviewer(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_gpxviewer)

    @ui.page("/image_cropper")
    async def show_image_cropper(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_image_cropper)

    @ui.page("/langs")
    async def show_langs(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_langs)

    @ui.page("/leaflet")
    async def show_leaflet(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_leaflet)

    @ui.page("/log_handler")
    async def show_log_handler_demo(client: Client):
        return await self.page(
            client, NiceGuiWidgetsDemo.show_log_element_handler_demo
        )

    @ui.page("/color_schema")
    async def show_color_schema(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_color_schema)

    @ui.page("/dictedit")
    async def show_dictedit(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_dictedit)

    @ui.page("/grid")
    async def show_grid(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_grid)

    @ui.page("/hideshow")
    async def show_hide_show(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_hide_show_demo)

    @ui.page("/taskrunner")
    async def show_taskrunner_demo(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_taskrunner_demo)

    @ui.page("/tristate")
    async def show_tristate_demo(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_tristate_demo)

    @ui.page("/pdfviewer")
    async def show_pdf_viewer(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_pdf_viewer)

    @ui.page("/issue1786")
    async def show_issue_1786(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_issue_1786)

    @ui.page("/stars")
    async def show_debounce_demo(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_debounce_demo)

wikipedia

wikipedia.py Conduct detailed searches against the Wikipedia API using the 'query' action to fetch article titles, summaries, and direct URLs.

Created on 2024-06-22 @author: wf

WikipediaSearch

A class for performing search queries against Wikipedia. This class leverages the MediaWiki API to obtain detailed article information including titles, summaries, and direct URLs.

Source code in ngwidgets/wikipedia.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class WikipediaSearch:
    """
    A class for performing search queries against Wikipedia. This class leverages the MediaWiki API
    to obtain detailed article information including titles, summaries, and direct URLs.
    """

    def __init__(self, country="en"):
        """
        Initializes the WikipediaSearch class with the base URL for the Wikipedia API tailored to the specified language.

        Args:
            country (str): country code to configure the API URL, defaults to 'en' for English.
        """
        self.country = country
        self.base_url = f"https://{country}.wikipedia.org/w/api.php"
        self.session = (
            requests.Session()
        )  # Using a session for connection pooling to enhance performance.

    def search(self, query, limit=10, explain_text=False):
        """
        Performs a detailed search on Wikipedia by leveraging the 'query' action to fetch summaries,
        in addition to using the 'opensearch' API for quick search results.

        Args:
            query (str): The search string to query Wikipedia with.
            limit (int): The maximum number of search results to return. Default is 10.
            explain_text (bool): If True, return extracts as plain text. Default is True.

        Returns:
            list: A list of dictionaries, each containing the title, summary, and direct URL of an article.
        """

        # Setup parameters for the query action to fetch detailed information
        params = {
            "action": "query",
            "list": "search",
            "srsearch": query,
            "format": "json",
            "utf8": 1,
            "prop": "info|extracts",  # Fetch page info and extracts
            "inprop": "url",  # Include the direct URL of each page
            "exintro": True,  # Only the introduction of each page
            "explaintext": explain_text,  # Return extracts as plain text
            "exlimit": "max",  # Maximum number of extracts
            "srlimit": limit,  # Limit the number of search results
        }
        response = self.session.get(self.base_url, params=params)
        search_results = response.json().get("query", {}).get("search", [])

        # Formulate results including summaries and URLs
        detailed_results = []
        for result in search_results:
            # Fetch page details with the correct use of parameters
            page_id = result["pageid"]
            page_params = {
                "action": "query",
                "prop": "info",
                "pageids": str(page_id),
                "inprop": "url",
                "format": "json",
            }
            page_response = self.session.get(self.base_url, params=page_params)
            page_info = page_response.json()["query"]["pages"][str(page_id)]
            url = page_info["fullurl"]  # Direct URL from the API

            detailed_result = {
                "title": result["title"],
                "summary": result.get("snippet", "No summary available."),
                "url": url,
            }
            detailed_results.append(detailed_result)

        return detailed_results

__init__(country='en')

Initializes the WikipediaSearch class with the base URL for the Wikipedia API tailored to the specified language.

Parameters:

Name Type Description Default
country str

country code to configure the API URL, defaults to 'en' for English.

'en'
Source code in ngwidgets/wikipedia.py
19
20
21
22
23
24
25
26
27
28
29
30
def __init__(self, country="en"):
    """
    Initializes the WikipediaSearch class with the base URL for the Wikipedia API tailored to the specified language.

    Args:
        country (str): country code to configure the API URL, defaults to 'en' for English.
    """
    self.country = country
    self.base_url = f"https://{country}.wikipedia.org/w/api.php"
    self.session = (
        requests.Session()
    )  # Using a session for connection pooling to enhance performance.

search(query, limit=10, explain_text=False)

Performs a detailed search on Wikipedia by leveraging the 'query' action to fetch summaries, in addition to using the 'opensearch' API for quick search results.

Parameters:

Name Type Description Default
query str

The search string to query Wikipedia with.

required
limit int

The maximum number of search results to return. Default is 10.

10
explain_text bool

If True, return extracts as plain text. Default is True.

False

Returns:

Name Type Description
list

A list of dictionaries, each containing the title, summary, and direct URL of an article.

Source code in ngwidgets/wikipedia.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def search(self, query, limit=10, explain_text=False):
    """
    Performs a detailed search on Wikipedia by leveraging the 'query' action to fetch summaries,
    in addition to using the 'opensearch' API for quick search results.

    Args:
        query (str): The search string to query Wikipedia with.
        limit (int): The maximum number of search results to return. Default is 10.
        explain_text (bool): If True, return extracts as plain text. Default is True.

    Returns:
        list: A list of dictionaries, each containing the title, summary, and direct URL of an article.
    """

    # Setup parameters for the query action to fetch detailed information
    params = {
        "action": "query",
        "list": "search",
        "srsearch": query,
        "format": "json",
        "utf8": 1,
        "prop": "info|extracts",  # Fetch page info and extracts
        "inprop": "url",  # Include the direct URL of each page
        "exintro": True,  # Only the introduction of each page
        "explaintext": explain_text,  # Return extracts as plain text
        "exlimit": "max",  # Maximum number of extracts
        "srlimit": limit,  # Limit the number of search results
    }
    response = self.session.get(self.base_url, params=params)
    search_results = response.json().get("query", {}).get("search", [])

    # Formulate results including summaries and URLs
    detailed_results = []
    for result in search_results:
        # Fetch page details with the correct use of parameters
        page_id = result["pageid"]
        page_params = {
            "action": "query",
            "prop": "info",
            "pageids": str(page_id),
            "inprop": "url",
            "format": "json",
        }
        page_response = self.session.get(self.base_url, params=page_params)
        page_info = page_response.json()["query"]["pages"][str(page_id)]
        url = page_info["fullurl"]  # Direct URL from the API

        detailed_result = {
            "title": result["title"],
            "summary": result.get("snippet", "No summary available."),
            "url": url,
        }
        detailed_results.append(detailed_result)

    return detailed_results

yamlable

Created on 2023-12-08, Extended on 2023-16-12 and 2024-01-25

@author: wf, ChatGPT

Prompts for the development and extension of the 'YamlAble' class within the 'yamable' module:

  1. Develop 'YamlAble' class in 'yamable' module. It should convert dataclass instances to/from YAML.
  2. Implement methods for YAML block scalar style and exclude None values in 'YamlAble' class.
  3. Add functionality to remove None values from dataclass instances before YAML conversion.
  4. Ensure 'YamlAble' processes only dataclass instances, with error handling for non-dataclass objects.
  5. Extend 'YamlAble' for JSON serialization and deserialization.
  6. Add methods for saving/loading dataclass instances to/from YAML and JSON files in 'YamlAble'.
  7. Implement loading of dataclass instances from URLs for both YAML and JSON in 'YamlAble'.
  8. Write tests for 'YamlAble' within the pyLodStorage context. Use 'samples 2' example from pyLoDStorage https://github.com/WolfgangFahl/pyLoDStorage/blob/master/lodstorage/sample2.py as a reference.
  9. Ensure tests cover YAML/JSON serialization, deserialization, and file I/O operations, using the sample-based approach..
  10. Use Google-style docstrings, comments, and type hints in 'YamlAble' class and tests.
  11. Adhere to instructions and seek clarification for any uncertainties.
  12. Add @lod_storable annotation support that will automatically YamlAble support and add @dataclass and @dataclass_json prerequisite behavior to a class

DateConvert

date converter

Source code in ngwidgets/yamlable.py
76
77
78
79
80
81
82
83
84
class DateConvert:
    """
    date converter
    """

    @classmethod
    def iso_date_to_datetime(cls, iso_date: str) -> datetime.date:
        date = datetime.strptime(iso_date, "%Y-%m-%d").date() if iso_date else None
        return date

YamlAble

Bases: Generic[T]

An extended YAML handler class for converting dataclass objects to and from YAML format, and handling loading from and saving to files and URLs.

Source code in ngwidgets/yamlable.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
class YamlAble(Generic[T]):
    """
    An extended YAML handler class for converting dataclass objects to and from YAML format,
    and handling loading from and saving to files and URLs.
    """

    def _yaml_setup(self):
        """
        Initializes the YamAble handler, setting up custom representers and preparing it for various operations.
        """
        if not is_dataclass(self):
            raise ValueError("I must be a dataclass instance.")
        if not hasattr(self, "_yaml_dumper"):
            self._yaml_dumper = yaml.Dumper
            self._yaml_dumper.ignore_aliases = lambda *_args: True
            self._yaml_dumper.add_representer(type(None), self.represent_none)
            self._yaml_dumper.add_representer(str, self.represent_literal)

    def represent_none(self, _, __) -> yaml.Node:
        """
        Custom representer for ignoring None values in the YAML output.
        """
        return self._yaml_dumper.represent_scalar("tag:yaml.org,2002:null", "")

    def represent_literal(self, dumper: yaml.Dumper, data: str) -> yaml.Node:
        """
        Custom representer for block scalar style for strings.
        """
        if "\n" in data:
            return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
        return dumper.represent_scalar("tag:yaml.org,2002:str", data)

    def to_yaml(
        self,
        ignore_none: bool = True,
        ignore_underscore: bool = True,
        allow_unicode: bool = True,
        sort_keys: bool = False,
    ) -> str:
        """
        Converts this dataclass object to a YAML string, with options to omit None values and/or underscore-prefixed variables,
        and using block scalar style for strings.

        Args:
            ignore_none: Flag to indicate whether None values should be removed from the YAML output.
            ignore_underscore: Flag to indicate whether attributes starting with an underscore should be excluded from the YAML output.
            allow_unicode: Flag to indicate whether to allow unicode characters in the output.
            sort_keys: Flag to indicate whether to sort the dictionary keys in the output.

        Returns:
            A string representation of the dataclass object in YAML format.
        """
        obj_dict = asdict(self)
        self._yaml_setup()
        clean_dict = self.remove_ignored_values(
            obj_dict, ignore_none, ignore_underscore
        )
        yaml_str = yaml.dump(
            clean_dict,
            Dumper=self._yaml_dumper,
            default_flow_style=False,
            allow_unicode=allow_unicode,
            sort_keys=sort_keys,
        )
        return yaml_str

    @classmethod
    def from_yaml(cls: Type[T], yaml_str: str) -> T:
        """
        Deserializes a YAML string to a dataclass instance.

        Args:
            yaml_str (str): A string containing YAML formatted data.

        Returns:
            T: An instance of the dataclass.
        """
        data: dict[str, Any] = yaml.safe_load(yaml_str)
        instance: T = cls.from_dict(data)
        return instance

    @classmethod
    def load_from_yaml_file(cls: Type[T], filename: str) -> T:
        """
        Loads a dataclass instance from a YAML file.

        Args:
            filename (str): The path to the YAML file.

        Returns:
            T: An instance of the dataclass.
        """
        with open(filename, "r") as file:
            yaml_str: str = file.read()
        instance: T = cls.from_yaml(yaml_str)
        return instance

    @classmethod
    def load_from_yaml_url(cls: Type[T], url: str) -> T:
        """
        Loads a dataclass instance from a YAML string obtained from a URL.

        Args:
            url (str): The URL pointing to the YAML data.

        Returns:
            T: An instance of the dataclass.
        """
        yaml_str: str = cls.read_from_url(url)
        instance: T = cls.from_yaml(yaml_str)
        return instance

    def save_to_yaml_file(self, filename: str):
        """
        Saves the current dataclass instance to a YAML file.

        Args:
            filename (str): The path where the YAML file will be saved.
        """
        yaml_content: str = self.to_yaml()
        with open(filename, "w") as file:
            file.write(yaml_content)

    @classmethod
    def load_from_json_file(cls: Type[T], filename: str) -> T:
        """
        Loads a dataclass instance from a JSON file.

        Args:
            filename (str): The path to the JSON file.

        Returns:
            T: An instance of the dataclass.
        """
        with open(filename, "r") as file:
            json_str: str = file.read()
        instance: T = cls.from_json(json_str)
        return instance

    @classmethod
    def load_from_json_url(cls: Type[T], url: str) -> T:
        """
        Loads a dataclass instance from a JSON string obtained from a URL.

        Args:
            url (str): The URL pointing to the JSON data.

        Returns:
            T: An instance of the dataclass.
        """
        json_str: str = cls.read_from_url(url)
        instance: T = cls.from_json(json_str)
        return instance

    def save_to_json_file(self, filename: str, **kwargs):
        """
        Saves the current dataclass instance to a JSON file.

        Args:
            filename (str): The path where the JSON file will be saved.
            **kwargs: Additional keyword arguments for the `to_json` method.
        """
        json_content: str = self.to_json(**kwargs)
        with open(filename, "w") as file:
            file.write(json_content)

    @classmethod
    def read_from_url(cls, url: str) -> str:
        """
        Helper method to fetch content from a URL.
        """
        with urllib.request.urlopen(url) as response:
            if response.status == 200:
                return response.read().decode()
            else:
                raise Exception(f"Unable to load data from URL: {url}")

    @classmethod
    def remove_ignored_values(
        cls,
        value: Any,
        ignore_none: bool = True,
        ignore_underscore: bool = False,
        ignore_empty: bool = True,
    ) -> Any:
        """
        Recursively removes specified types of values from a dictionary or list.
        By default, it removes keys with None values. Optionally, it can also remove keys starting with an underscore.

        Args:
            value: The value to process (dictionary, list, or other).
            ignore_none: Flag to indicate whether None values should be removed.
            ignore_underscore: Flag to indicate whether keys starting with an underscore should be removed.
            ignore_empty: Flag to indicate whether empty collections should be removed.
        """

        def is_valid(v):
            """Check if the value is valid based on the specified flags."""
            if ignore_none and v is None:
                return False
            if ignore_empty:
                if isinstance(v, Mapping) and not v:
                    return False  # Empty dictionary
                if (
                    isinstance(v, Iterable)
                    and not isinstance(v, (str, bytes))
                    and not v
                ):
                    return (
                        False  # Empty list, set, tuple, etc., but not string or bytes
                    )
            return True

        if isinstance(value, Mapping):
            value = {
                k: YamlAble.remove_ignored_values(
                    v, ignore_none, ignore_underscore, ignore_empty
                )
                for k, v in value.items()
                if is_valid(v) and (not ignore_underscore or not k.startswith("_"))
            }
        elif isinstance(value, Iterable) and not isinstance(value, (str, bytes)):
            value = [
                YamlAble.remove_ignored_values(
                    v, ignore_none, ignore_underscore, ignore_empty
                )
                for v in value
                if is_valid(v)
            ]
        return value

    @classmethod
    def from_dict2(cls: Type[T], data: dict) -> T:
        """
        Creates an instance of a dataclass from a dictionary, typically used in deserialization.
        """
        if not data:
            return None
        instance = from_dict(data_class=cls, data=data)
        return instance

from_dict2(data) classmethod

Creates an instance of a dataclass from a dictionary, typically used in deserialization.

Source code in ngwidgets/yamlable.py
318
319
320
321
322
323
324
325
326
@classmethod
def from_dict2(cls: Type[T], data: dict) -> T:
    """
    Creates an instance of a dataclass from a dictionary, typically used in deserialization.
    """
    if not data:
        return None
    instance = from_dict(data_class=cls, data=data)
    return instance

from_yaml(yaml_str) classmethod

Deserializes a YAML string to a dataclass instance.

Parameters:

Name Type Description Default
yaml_str str

A string containing YAML formatted data.

required

Returns:

Name Type Description
T T

An instance of the dataclass.

Source code in ngwidgets/yamlable.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
@classmethod
def from_yaml(cls: Type[T], yaml_str: str) -> T:
    """
    Deserializes a YAML string to a dataclass instance.

    Args:
        yaml_str (str): A string containing YAML formatted data.

    Returns:
        T: An instance of the dataclass.
    """
    data: dict[str, Any] = yaml.safe_load(yaml_str)
    instance: T = cls.from_dict(data)
    return instance

load_from_json_file(filename) classmethod

Loads a dataclass instance from a JSON file.

Parameters:

Name Type Description Default
filename str

The path to the JSON file.

required

Returns:

Name Type Description
T T

An instance of the dataclass.

Source code in ngwidgets/yamlable.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
@classmethod
def load_from_json_file(cls: Type[T], filename: str) -> T:
    """
    Loads a dataclass instance from a JSON file.

    Args:
        filename (str): The path to the JSON file.

    Returns:
        T: An instance of the dataclass.
    """
    with open(filename, "r") as file:
        json_str: str = file.read()
    instance: T = cls.from_json(json_str)
    return instance

load_from_json_url(url) classmethod

Loads a dataclass instance from a JSON string obtained from a URL.

Parameters:

Name Type Description Default
url str

The URL pointing to the JSON data.

required

Returns:

Name Type Description
T T

An instance of the dataclass.

Source code in ngwidgets/yamlable.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
@classmethod
def load_from_json_url(cls: Type[T], url: str) -> T:
    """
    Loads a dataclass instance from a JSON string obtained from a URL.

    Args:
        url (str): The URL pointing to the JSON data.

    Returns:
        T: An instance of the dataclass.
    """
    json_str: str = cls.read_from_url(url)
    instance: T = cls.from_json(json_str)
    return instance

load_from_yaml_file(filename) classmethod

Loads a dataclass instance from a YAML file.

Parameters:

Name Type Description Default
filename str

The path to the YAML file.

required

Returns:

Name Type Description
T T

An instance of the dataclass.

Source code in ngwidgets/yamlable.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
@classmethod
def load_from_yaml_file(cls: Type[T], filename: str) -> T:
    """
    Loads a dataclass instance from a YAML file.

    Args:
        filename (str): The path to the YAML file.

    Returns:
        T: An instance of the dataclass.
    """
    with open(filename, "r") as file:
        yaml_str: str = file.read()
    instance: T = cls.from_yaml(yaml_str)
    return instance

load_from_yaml_url(url) classmethod

Loads a dataclass instance from a YAML string obtained from a URL.

Parameters:

Name Type Description Default
url str

The URL pointing to the YAML data.

required

Returns:

Name Type Description
T T

An instance of the dataclass.

Source code in ngwidgets/yamlable.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@classmethod
def load_from_yaml_url(cls: Type[T], url: str) -> T:
    """
    Loads a dataclass instance from a YAML string obtained from a URL.

    Args:
        url (str): The URL pointing to the YAML data.

    Returns:
        T: An instance of the dataclass.
    """
    yaml_str: str = cls.read_from_url(url)
    instance: T = cls.from_yaml(yaml_str)
    return instance

read_from_url(url) classmethod

Helper method to fetch content from a URL.

Source code in ngwidgets/yamlable.py
253
254
255
256
257
258
259
260
261
262
@classmethod
def read_from_url(cls, url: str) -> str:
    """
    Helper method to fetch content from a URL.
    """
    with urllib.request.urlopen(url) as response:
        if response.status == 200:
            return response.read().decode()
        else:
            raise Exception(f"Unable to load data from URL: {url}")

remove_ignored_values(value, ignore_none=True, ignore_underscore=False, ignore_empty=True) classmethod

Recursively removes specified types of values from a dictionary or list. By default, it removes keys with None values. Optionally, it can also remove keys starting with an underscore.

Parameters:

Name Type Description Default
value Any

The value to process (dictionary, list, or other).

required
ignore_none bool

Flag to indicate whether None values should be removed.

True
ignore_underscore bool

Flag to indicate whether keys starting with an underscore should be removed.

False
ignore_empty bool

Flag to indicate whether empty collections should be removed.

True
Source code in ngwidgets/yamlable.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
@classmethod
def remove_ignored_values(
    cls,
    value: Any,
    ignore_none: bool = True,
    ignore_underscore: bool = False,
    ignore_empty: bool = True,
) -> Any:
    """
    Recursively removes specified types of values from a dictionary or list.
    By default, it removes keys with None values. Optionally, it can also remove keys starting with an underscore.

    Args:
        value: The value to process (dictionary, list, or other).
        ignore_none: Flag to indicate whether None values should be removed.
        ignore_underscore: Flag to indicate whether keys starting with an underscore should be removed.
        ignore_empty: Flag to indicate whether empty collections should be removed.
    """

    def is_valid(v):
        """Check if the value is valid based on the specified flags."""
        if ignore_none and v is None:
            return False
        if ignore_empty:
            if isinstance(v, Mapping) and not v:
                return False  # Empty dictionary
            if (
                isinstance(v, Iterable)
                and not isinstance(v, (str, bytes))
                and not v
            ):
                return (
                    False  # Empty list, set, tuple, etc., but not string or bytes
                )
        return True

    if isinstance(value, Mapping):
        value = {
            k: YamlAble.remove_ignored_values(
                v, ignore_none, ignore_underscore, ignore_empty
            )
            for k, v in value.items()
            if is_valid(v) and (not ignore_underscore or not k.startswith("_"))
        }
    elif isinstance(value, Iterable) and not isinstance(value, (str, bytes)):
        value = [
            YamlAble.remove_ignored_values(
                v, ignore_none, ignore_underscore, ignore_empty
            )
            for v in value
            if is_valid(v)
        ]
    return value

represent_literal(dumper, data)

Custom representer for block scalar style for strings.

Source code in ngwidgets/yamlable.py
111
112
113
114
115
116
117
def represent_literal(self, dumper: yaml.Dumper, data: str) -> yaml.Node:
    """
    Custom representer for block scalar style for strings.
    """
    if "\n" in data:
        return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
    return dumper.represent_scalar("tag:yaml.org,2002:str", data)

represent_none(_, __)

Custom representer for ignoring None values in the YAML output.

Source code in ngwidgets/yamlable.py
105
106
107
108
109
def represent_none(self, _, __) -> yaml.Node:
    """
    Custom representer for ignoring None values in the YAML output.
    """
    return self._yaml_dumper.represent_scalar("tag:yaml.org,2002:null", "")

save_to_json_file(filename, **kwargs)

Saves the current dataclass instance to a JSON file.

Parameters:

Name Type Description Default
filename str

The path where the JSON file will be saved.

required
**kwargs

Additional keyword arguments for the to_json method.

{}
Source code in ngwidgets/yamlable.py
241
242
243
244
245
246
247
248
249
250
251
def save_to_json_file(self, filename: str, **kwargs):
    """
    Saves the current dataclass instance to a JSON file.

    Args:
        filename (str): The path where the JSON file will be saved.
        **kwargs: Additional keyword arguments for the `to_json` method.
    """
    json_content: str = self.to_json(**kwargs)
    with open(filename, "w") as file:
        file.write(json_content)

save_to_yaml_file(filename)

Saves the current dataclass instance to a YAML file.

Parameters:

Name Type Description Default
filename str

The path where the YAML file will be saved.

required
Source code in ngwidgets/yamlable.py
199
200
201
202
203
204
205
206
207
208
def save_to_yaml_file(self, filename: str):
    """
    Saves the current dataclass instance to a YAML file.

    Args:
        filename (str): The path where the YAML file will be saved.
    """
    yaml_content: str = self.to_yaml()
    with open(filename, "w") as file:
        file.write(yaml_content)

to_yaml(ignore_none=True, ignore_underscore=True, allow_unicode=True, sort_keys=False)

Converts this dataclass object to a YAML string, with options to omit None values and/or underscore-prefixed variables, and using block scalar style for strings.

Parameters:

Name Type Description Default
ignore_none bool

Flag to indicate whether None values should be removed from the YAML output.

True
ignore_underscore bool

Flag to indicate whether attributes starting with an underscore should be excluded from the YAML output.

True
allow_unicode bool

Flag to indicate whether to allow unicode characters in the output.

True
sort_keys bool

Flag to indicate whether to sort the dictionary keys in the output.

False

Returns:

Type Description
str

A string representation of the dataclass object in YAML format.

Source code in ngwidgets/yamlable.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def to_yaml(
    self,
    ignore_none: bool = True,
    ignore_underscore: bool = True,
    allow_unicode: bool = True,
    sort_keys: bool = False,
) -> str:
    """
    Converts this dataclass object to a YAML string, with options to omit None values and/or underscore-prefixed variables,
    and using block scalar style for strings.

    Args:
        ignore_none: Flag to indicate whether None values should be removed from the YAML output.
        ignore_underscore: Flag to indicate whether attributes starting with an underscore should be excluded from the YAML output.
        allow_unicode: Flag to indicate whether to allow unicode characters in the output.
        sort_keys: Flag to indicate whether to sort the dictionary keys in the output.

    Returns:
        A string representation of the dataclass object in YAML format.
    """
    obj_dict = asdict(self)
    self._yaml_setup()
    clean_dict = self.remove_ignored_values(
        obj_dict, ignore_none, ignore_underscore
    )
    yaml_str = yaml.dump(
        clean_dict,
        Dumper=self._yaml_dumper,
        default_flow_style=False,
        allow_unicode=allow_unicode,
        sort_keys=sort_keys,
    )
    return yaml_str

lod_storable(cls)

Decorator to make a class LoDStorable by inheriting from YamlAble. This decorator also ensures the class is a dataclass and has JSON serialization/deserialization capabilities.

Source code in ngwidgets/yamlable.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def lod_storable(cls):
    """
    Decorator to make a class LoDStorable by
    inheriting from YamlAble.
    This decorator also ensures the class is a
    dataclass and has JSON serialization/deserialization
    capabilities.
    """
    cls = dataclass(cls)  # Apply the @dataclass decorator
    cls = dataclass_json(cls)  # Apply the @dataclass_json decorator

    class LoDStorable(YamlAble, cls):
        """
        decorator class
        """

        __qualname__ = cls.__qualname__
        pass

    LoDStorable.__name__ = cls.__name__
    LoDStorable.__doc__ = cls.__doc__

    return LoDStorable