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

basetest

Created on 2021-08-19

@author: wf

Basetest

Bases: TestCase

base test case

Source code in ngwidgets/basetest.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
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

inPublicCI() staticmethod

are we running in a public Continuous Integration Environment?

Source code in ngwidgets/basetest.py
33
34
35
36
37
38
39
40
@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
42
43
44
45
@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
19
20
21
22
23
24
25
26
27
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)

cmd

Created on 2023-09-10

@author: wf

WebserverCmd

Bases: object

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
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
class WebserverCmd(object):
    """
    Baseclass for command line handling of Webservers
    """

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

    def getArgParser(self, description: str = None, version_msg=None) -> ArgumentParser:
        """
        Setup command line argument parser

        Args:
            description(str): the description
            version_msg(str): the version message

        Returns:
            ArgumentParser: the argument parser
        """
        if description is None:
            description = self.version.description
        if version_msg is None:
            version_msg = self.program_version_message
        parser = ArgumentParser(
            description=description, formatter_class=RawDescriptionHelpFormatter
        )
        parser.add_argument(
            "-a",
            "--about",
            help="show about info [default: %(default)s]",
            action="store_true",
        )
        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 [default: %(default)s]",
        )
        parser.add_argument(
            "-d",
            "--debug",
            action="store_true",
            help="show debug info [default: %(default)s]",
        )
        parser.add_argument("--debugServer", help="remote debug Server")
        parser.add_argument(
            "--debugPort", type=int, help="remote debug Port", default=5678
        )
        parser.add_argument(
            "--debugRemotePath",
            help="remote debug Server path mapping - remotePath - path on debug server",
        )
        parser.add_argument(
            "--debugLocalPath",
            help="remote debug Server path mapping - localPath - path on machine where python runs",
        )

        parser.add_argument(
            "-l",
            "--local",
            action="store_true",
            help="run with local file system access [default: %(default)s]",
        )
        parser.add_argument("-i", "--input", help="input file")

        parser.add_argument(
            "-rol",
            "--render_on_load",
            action="store_true",
            help="render on load [default: %(default)s]",
        )

        parser.add_argument(
            "--host",
            default="localhost",
            help="the host to serve / listen from [default: %(default)s]",
        )
        parser.add_argument(
            "--port",
            type=int,
            default=self.config.default_port,
            help="the port to serve from [default: %(default)s]",
        )
        parser.add_argument(
            "-s",
            "--serve",
            action="store_true",
            help="start webserver [default: %(default)s]",
        )
        parser.add_argument("-V", "--version", action="version", version=version_msg)
        return parser

    def handle_args(self) -> bool:
        handled = False
        if self.args.apache:
            print(self.to_apache_config(self.config, self.args.apache))
        if self.args.about:
            print(self.program_version_message)
            print(f"see {self.version.doc_url}")
            webbrowser.open(self.version.doc_url)
            handled = True
        if self.args.client:
            url = f"http://{self.args.host}:{self.args.port}"
            webbrowser.open(url)
            handled = True
        if self.args.serve:
            # instantiate the webserver
            ws = self.webserver_cls()
            ws.run(self.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_parse(self, argv: list = None):
        """
        parse the argument lists and prepare

        Args:
            argv(list): list of command line arguments

        """
        if argv is None:
            argv = sys.argv[1:]
        self.argv = argv
        self.program_name = self.version.name
        self.program_version = f"v{self.version.version}"
        self.program_build_date = str(self.version.date)
        self.program_version_message = (
            f"{self.program_name} ({self.program_version},{self.program_build_date})"
        )
        self.parser = self.getArgParser(
            description=self.version.description,
            version_msg=self.program_version_message,
        )
        self.args = self.parser.parse_args(argv)
        return self.args

    def cmd_main(self, argv: None) -> int:
        """
        main program as an instance

        Args:
            argv(list): list of command line arguments

        Returns:
            int: exit code - 0 of all went well 1 for keyboard interrupt and 2 for exceptions
        """
        try:
            self.cmd_parse(argv)
            if len(self.argv) < 1:
                self.parser.print_usage()
                sys.exit(1)
            self.handle_args()
        except KeyboardInterrupt:
            ### handle keyboard interrupt ###
            self.exit_code = 1
        except Exception as e:
            if self.debug:
                raise (e)
            indent = len(self.program_name) * " "
            sys.stderr.write(self.program_name + ": " + repr(e) + "\n")
            sys.stderr.write(indent + "  for help use --help")
            if self.args.debug:
                print(traceback.format_exc())
            self.exit_code = 2

        return self.exit_code

__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
    """
    self.config = config
    self.version = config.version
    self.debug = debug
    self.webserver_cls = webserver_cls
    self.exit_code = 0

cmd_main(argv)

main program as an instance

Parameters:

Name Type Description Default
argv(list)

list of command line arguments

required

Returns:

Name Type Description
int int

exit code - 0 of all went well 1 for keyboard interrupt and 2 for exceptions

Source code in ngwidgets/cmd.py
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
def cmd_main(self, argv: None) -> int:
    """
    main program as an instance

    Args:
        argv(list): list of command line arguments

    Returns:
        int: exit code - 0 of all went well 1 for keyboard interrupt and 2 for exceptions
    """
    try:
        self.cmd_parse(argv)
        if len(self.argv) < 1:
            self.parser.print_usage()
            sys.exit(1)
        self.handle_args()
    except KeyboardInterrupt:
        ### handle keyboard interrupt ###
        self.exit_code = 1
    except Exception as e:
        if self.debug:
            raise (e)
        indent = len(self.program_name) * " "
        sys.stderr.write(self.program_name + ": " + repr(e) + "\n")
        sys.stderr.write(indent + "  for help use --help")
        if self.args.debug:
            print(traceback.format_exc())
        self.exit_code = 2

    return self.exit_code

cmd_parse(argv=None)

parse the argument lists and prepare

Parameters:

Name Type Description Default
argv(list)

list of command line arguments

required
Source code in ngwidgets/cmd.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def cmd_parse(self, argv: list = None):
    """
    parse the argument lists and prepare

    Args:
        argv(list): list of command line arguments

    """
    if argv is None:
        argv = sys.argv[1:]
    self.argv = argv
    self.program_name = self.version.name
    self.program_version = f"v{self.version.version}"
    self.program_build_date = str(self.version.date)
    self.program_version_message = (
        f"{self.program_name} ({self.program_version},{self.program_build_date})"
    )
    self.parser = self.getArgParser(
        description=self.version.description,
        version_msg=self.program_version_message,
    )
    self.args = self.parser.parse_args(argv)
    return self.args

getArgParser(description=None, version_msg=None)

Setup command line argument parser

Parameters:

Name Type Description Default
description(str)

the description

required
version_msg(str)

the version message

required

Returns:

Name Type Description
ArgumentParser ArgumentParser

the argument parser

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
 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
def getArgParser(self, description: str = None, version_msg=None) -> ArgumentParser:
    """
    Setup command line argument parser

    Args:
        description(str): the description
        version_msg(str): the version message

    Returns:
        ArgumentParser: the argument parser
    """
    if description is None:
        description = self.version.description
    if version_msg is None:
        version_msg = self.program_version_message
    parser = ArgumentParser(
        description=description, formatter_class=RawDescriptionHelpFormatter
    )
    parser.add_argument(
        "-a",
        "--about",
        help="show about info [default: %(default)s]",
        action="store_true",
    )
    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 [default: %(default)s]",
    )
    parser.add_argument(
        "-d",
        "--debug",
        action="store_true",
        help="show debug info [default: %(default)s]",
    )
    parser.add_argument("--debugServer", help="remote debug Server")
    parser.add_argument(
        "--debugPort", type=int, help="remote debug Port", default=5678
    )
    parser.add_argument(
        "--debugRemotePath",
        help="remote debug Server path mapping - remotePath - path on debug server",
    )
    parser.add_argument(
        "--debugLocalPath",
        help="remote debug Server path mapping - localPath - path on machine where python runs",
    )

    parser.add_argument(
        "-l",
        "--local",
        action="store_true",
        help="run with local file system access [default: %(default)s]",
    )
    parser.add_argument("-i", "--input", help="input file")

    parser.add_argument(
        "-rol",
        "--render_on_load",
        action="store_true",
        help="render on load [default: %(default)s]",
    )

    parser.add_argument(
        "--host",
        default="localhost",
        help="the host to serve / listen from [default: %(default)s]",
    )
    parser.add_argument(
        "--port",
        type=int,
        default=self.config.default_port,
        help="the port to serve from [default: %(default)s]",
    )
    parser.add_argument(
        "-s",
        "--serve",
        action="store_true",
        help="start webserver [default: %(default)s]",
    )
    parser.add_argument("-V", "--version", action="version", version=version_msg)
    return parser

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
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
    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

required
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.parent.remove(self.spinner)
                except KeyError:
                    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

required
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.parent.remove(self.spinner)
            except KeyError:
                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
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 _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
192
193
194
195
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)

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"]

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

required

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

required

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

required
handler(Callable)

handler function to call on selection

required
filter_func(Callable)

optional filter function

required
create_ui(bool)

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

required
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([])

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
 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
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 = ui.log(max_lines=max_lines).classes(log_classes)
            if handle_logging:
                self.log_view_handler = LogElementHandler(self.log_view)
                self.webserver.logger.addHandler(self.log_view_handler)
        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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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
154
155
156
157
158
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
218
219
220
221
222
223
224
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
211
212
213
214
215
216
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
147
148
149
150
151
152
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
79
80
81
82
83
84
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
132
133
134
135
136
137
138
139
140
141
142
143
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
226
227
228
229
230
231
232
233
234
235
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

required
Source code in ngwidgets/input_webserver.py
100
101
102
103
104
105
106
107
108
109
110
111
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
86
87
88
89
90
91
92
93
94
95
96
97
98
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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
160
161
162
163
164
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
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 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 = ui.log(max_lines=max_lines).classes(log_classes)
        if handle_logging:
            self.log_view_handler = LogElementHandler(self.log_view)
            self.webserver.logger.addHandler(self.log_view_handler)
    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
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
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)
        self.logger = logging.getLogger()

        @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
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def __init__(self, config: WebserverConfig = None):
    """
    constructor
    """
    NiceGuiWebserver.__init__(self, config=config)
    self.logger = logging.getLogger()

    @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
42
43
44
45
46
47
48
49
50
51
52
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

https://github.com/zauberzeug/nicegui/blob/main/examples/map/leaflet.py

@author: rodja

modified 2023-08-16 to handle set_zoom and draw_path @author: wf

leaflet

Bases: element

nicegui Leaflet integration

see https://leafletjs.com/

Source code in ngwidgets/leaflet.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
class leaflet(ui.element, component="leaflet.js"):
    """
    nicegui Leaflet integration

    see https://leafletjs.com/
    """

    def __init__(self, version="1.7.1") -> None:
        super().__init__()
        ui.add_head_html(
            f'<link href="https://unpkg.com/leaflet@{version}/dist/leaflet.css" rel="stylesheet"/>'
        )
        ui.add_head_html(
            f'<script src="https://unpkg.com/leaflet@{version}/dist/leaflet.js"></script>'
        )

    def set_location(self, location: Tuple[float, float], zoom_level: int = 9) -> None:
        lat, lon = location
        self.run_method("set_location", lat, lon, zoom_level)

    def set_zoom_level(self, zoom_level: int):
        self.run_method("set_zoom_level", zoom_level)

    def draw_path(self, path: List[Tuple[float, float]]) -> None:
        """Draw a path on the map based on list of lat-long tuples."""
        self.run_method("draw_path", path)

draw_path(path)

Draw a path on the map based on list of lat-long tuples.

Source code in ngwidgets/leaflet.py
38
39
40
def draw_path(self, path: List[Tuple[float, float]]) -> None:
    """Draw a path on the map based on list of lat-long tuples."""
    self.run_method("draw_path", path)

llm

Created on 2023-06-23

@author: wf

LLM

Large Language Model access wrapper

Source code in ngwidgets/llm.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
class LLM:
    """
    Large Language Model access wrapper
    """

    # Define a dictionary that maps models to their size limits
    MODEL_SIZE_LIMITS = {
        "gpt-3.5-turbo": 4096,  # Adjust this limit as needed for other models
        "gpt-3.5-turbo-16k": 4096 * 4,
    }

    # Define a constant for the average token character length
    AVERAGE_TOKEN_CHAR_LEN = 4  # Adjust this value based on the model
    DEFAULT_MODEL = "gpt-3.5-turbo"

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

        Args:
            api_key(str): the access key
            model(str): the model to use
            prompt_filepath(str): the filepath for the prompt logging
        """
        self.model = model
        self.token_size_limit = LLM.MODEL_SIZE_LIMITS.get(
            model, 4096
        )  # Default to 4096 if model not found
        self.char_size_limit = self.token_size_limit * LLM.AVERAGE_TOKEN_CHAR_LEN
        openai_api_key = None
        if api_key:
            # If an API key is provided during object creation, set it.
            openai.api_key = api_key
        else:
            # Load the API key from the environment or a JSON file
            openai_api_key = os.getenv("OPENAI_API_KEY")
            json_file = Path.home() / ".openai" / "openai_api_key.json"

            if openai_api_key is None and json_file.is_file():
                with open(json_file, "r") as file:
                    data = json.load(file)
                    openai_api_key = data.get("OPENAI_API_KEY")

        if openai_api_key is None:
            if force_key:
                raise ValueError(
                    "No OpenAI API key found. Please set the 'OPENAI_API_KEY' environment variable or store it in `~/.openai/openai_api_key.json`."
                )
            else:
                return
        # set the global api key
        openai.api_key = openai_api_key
        # If prompts_filepath is None, use default path in the user's home directory with the current date
        if prompts_filepath is None:
            # Format: Year-Month-Day
            date_str = datetime.now().strftime("%Y-%m-%d")
            default_filename = f"prompts_{date_str}.yaml"  # Constructs the file name
            openai_dir = (
                Path.home() / ".openai"
            )  # Constructs the path to the .openai directory

            # Check if .openai directory exists, create if it doesn't
            if not openai_dir.exists():
                openai_dir.mkdir(parents=True, exist_ok=True)

            prompts_filepath = openai_dir / default_filename  # Constructs the full path

        self.prompts_filepath = prompts_filepath

        # Load or initialize the prompts file
        if prompts_filepath.is_file():
            self.prompts = Prompts.load_from_yaml_file(str(prompts_filepath))
        else:
            # If the file doesn't exist, create an empty Prompts object
            # You might want to handle directory creation here if .openai directory might not exist
            self.prompts = Prompts()

    def available(self):
        """
        check availability of API by making sure there is an api_key

        Returns:
            bool: True if the Large Language Model is available
        """
        return openai.api_key is not None

    def ask(self, prompt_text: str, model: str = None, temperature: float = 0.7) -> str:
        """
        ask a prompt

        Args:
            prompt_text(str): The text of the prompt to send to the model.
            model(str): the model to use - if None self.model is used
            temperature(float): Controls randomness in the response. Lower is more deterministic.
        Returns:
            str: the answer
        """
        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 timing the response
        start_time = datetime.now()

        # Interact with the API
        chat_completion = openai.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt_text}],
            temperature=temperature,  # Include the temperature parameter here
        )
        result = chat_completion.choices[0].message.content
        total_tokens = chat_completion.usage.total_tokens
        model_details = chat_completion.model

        # Calculate duration
        duration = (datetime.now() - start_time).total_seconds()

        # Create a new Prompt instance and append it to the prompts list
        new_prompt = Prompt(
            prompt=prompt_text,
            response=result,
            model=model,
            model_details=model_details,
            temperature=temperature,
            tokens=total_tokens,
            timestamp=datetime.now(),
            duration=duration,
        )
        start_save_time = time.time()

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

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

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

constructor

Parameters:

Name Type Description Default
api_key(str)

the access key

required
model(str)

the model to use

required
prompt_filepath(str)

the filepath for the prompt logging

required
Source code in ngwidgets/llm.py
 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
def __init__(
    self,
    api_key: str = None,
    model=DEFAULT_MODEL,
    force_key: bool = False,
    prompts_filepath: str = None,
):
    """
    constructor

    Args:
        api_key(str): the access key
        model(str): the model to use
        prompt_filepath(str): the filepath for the prompt logging
    """
    self.model = model
    self.token_size_limit = LLM.MODEL_SIZE_LIMITS.get(
        model, 4096
    )  # Default to 4096 if model not found
    self.char_size_limit = self.token_size_limit * LLM.AVERAGE_TOKEN_CHAR_LEN
    openai_api_key = None
    if api_key:
        # If an API key is provided during object creation, set it.
        openai.api_key = api_key
    else:
        # Load the API key from the environment or a JSON file
        openai_api_key = os.getenv("OPENAI_API_KEY")
        json_file = Path.home() / ".openai" / "openai_api_key.json"

        if openai_api_key is None and json_file.is_file():
            with open(json_file, "r") as file:
                data = json.load(file)
                openai_api_key = data.get("OPENAI_API_KEY")

    if openai_api_key is None:
        if force_key:
            raise ValueError(
                "No OpenAI API key found. Please set the 'OPENAI_API_KEY' environment variable or store it in `~/.openai/openai_api_key.json`."
            )
        else:
            return
    # set the global api key
    openai.api_key = openai_api_key
    # If prompts_filepath is None, use default path in the user's home directory with the current date
    if prompts_filepath is None:
        # Format: Year-Month-Day
        date_str = datetime.now().strftime("%Y-%m-%d")
        default_filename = f"prompts_{date_str}.yaml"  # Constructs the file name
        openai_dir = (
            Path.home() / ".openai"
        )  # Constructs the path to the .openai directory

        # Check if .openai directory exists, create if it doesn't
        if not openai_dir.exists():
            openai_dir.mkdir(parents=True, exist_ok=True)

        prompts_filepath = openai_dir / default_filename  # Constructs the full path

    self.prompts_filepath = prompts_filepath

    # Load or initialize the prompts file
    if prompts_filepath.is_file():
        self.prompts = Prompts.load_from_yaml_file(str(prompts_filepath))
    else:
        # If the file doesn't exist, create an empty Prompts object
        # You might want to handle directory creation here if .openai directory might not exist
        self.prompts = Prompts()

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

ask a prompt

Parameters:

Name Type Description Default
prompt_text(str)

The text of the prompt to send to the model.

required
model(str)

the model to use - if None self.model is used

required
temperature(float)

Controls randomness in the response. Lower is more deterministic.

required

Returns: str: the answer

Source code in ngwidgets/llm.py
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
def ask(self, prompt_text: str, model: str = None, temperature: float = 0.7) -> str:
    """
    ask a prompt

    Args:
        prompt_text(str): The text of the prompt to send to the model.
        model(str): the model to use - if None self.model is used
        temperature(float): Controls randomness in the response. Lower is more deterministic.
    Returns:
        str: the answer
    """
    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 timing the response
    start_time = datetime.now()

    # Interact with the API
    chat_completion = openai.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt_text}],
        temperature=temperature,  # Include the temperature parameter here
    )
    result = chat_completion.choices[0].message.content
    total_tokens = chat_completion.usage.total_tokens
    model_details = chat_completion.model

    # Calculate duration
    duration = (datetime.now() - start_time).total_seconds()

    # Create a new Prompt instance and append it to the prompts list
    new_prompt = Prompt(
        prompt=prompt_text,
        response=result,
        model=model,
        model_details=model_details,
        temperature=temperature,
        tokens=total_tokens,
        timestamp=datetime.now(),
        duration=duration,
    )
    start_save_time = time.time()

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

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

available()

check availability of API by making sure there is an api_key

Returns:

Name Type Description
bool

True if the Large Language Model is available

Source code in ngwidgets/llm.py
136
137
138
139
140
141
142
143
def available(self):
    """
    check availability of API by making sure there is an api_key

    Returns:
        bool: True if the Large Language Model is available
    """
    return openai.api_key is not None

Prompt

a single prompt with response

Source code in ngwidgets/llm.py
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 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

    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}")

Prompts

keep track of a series of prompts

Source code in ngwidgets/llm.py
44
45
46
47
48
49
50
@lod_storable
class Prompts:
    """
    keep track of a series of prompts
    """

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

local_filepicker

LocalFilePicker

Bases: dialog

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

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
class LocalFilePicker(ui.dialog):
    """see
    https://raw.githubusercontent.com/zauberzeug/nicegui/main/examples/local_file_picker/local_file_picker.py
    """

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

        This is a simple file picker that allows you to select a file from the local filesystem where NiceGUI is running.

        :param directory: The directory to start in.
        :param upper_limit: The directory to stop at (None: no limit, default: same as the starting directory).
        :param multiple: Whether to allow multiple files to be selected.
        :param show_hidden_files: Whether to show hidden files.
        """
        super().__init__()

        self.path = Path(directory).expanduser()
        if upper_limit is None:
            self.upper_limit = None
        else:
            self.upper_limit = Path(
                directory if upper_limit == ... else upper_limit
            ).expanduser()
        self.show_hidden_files = show_hidden_files

        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):
        if platform.system() == "Windows":
            import win32api

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

    def update_drive(self):
        self.path = Path(self.drives_toggle.value).expanduser()
        self.update_grid()

    def update_grid(self) -> None:
        paths = 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:
        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):
        rows = await ui.run_javascript(
            f"getElement({self.grid.id}).gridOptions.api.getSelectedRows()"
        )
        self.submit([r["path"] for r in rows])

__init__(directory, *, upper_limit=..., multiple=False, show_hidden_files=False)

Local File Picker

This is a simple file picker that allows you to select a file from the local filesystem where NiceGUI is running.

:param directory: The directory to start in. :param upper_limit: The directory to stop at (None: no limit, default: same as the starting directory). :param multiple: Whether to allow multiple files to be selected. :param show_hidden_files: Whether to show hidden files.

Source code in ngwidgets/local_filepicker.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
def __init__(
    self,
    directory: str,
    *,
    upper_limit: Optional[str] = ...,
    multiple: bool = False,
    show_hidden_files: bool = False,
) -> None:
    """Local File Picker

    This is a simple file picker that allows you to select a file from the local filesystem where NiceGUI is running.

    :param directory: The directory to start in.
    :param upper_limit: The directory to stop at (None: no limit, default: same as the starting directory).
    :param multiple: Whether to allow multiple files to be selected.
    :param show_hidden_files: Whether to show hidden files.
    """
    super().__init__()

    self.path = Path(directory).expanduser()
    if upper_limit is None:
        self.upper_limit = None
    else:
        self.upper_limit = Path(
            directory if upper_limit == ... else upper_limit
        ).expanduser()
    self.show_hidden_files = show_hidden_files

    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()

lod_grid

Created on 2023-10-3

@author: wf

GridConfig dataclass

Configuration for initializing a ListOfDictsGrid.

Source code in ngwidgets/lod_grid.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
@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 = "ag-theme-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
    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
 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
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 = {}
        try:
            if self.config.with_buttons:
                self.setup_button_row()
            # 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.
        """
        if self.config.debug:
            # Print a stack trace to stderr
            print("Exception caught in ListOfDictsGrid:", file=sys.stderr)
            traceback.print_exc()
            # Optionally, re-raise the exception for further debugging.
            raise ex
        elif self.config.exception_callback:
            self.config.exception_callback(ex)
        else:
            # If not in debug mode, notify the user with a general error message.
            # Ensure that ui.notify or a similar method is available and properly configured.
            ui.notify(
                f"Unhandled exception {str(ex)} occurred in ListOfDictsGrid",
                type="error",
            )

    def update_index(self, lenient: bool = False):
        """
        update the index based on the given key column
        """
        self.lod_index = {}
        if self.lod:
            for row_index, row in enumerate(self.lod):
                if self.config.key_col in row:
                    key_value = row[self.config.key_col]
                    self.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

    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_column_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_all_rows(self):
        """
        select all my ag_grid rows
        """
        self.ag_grid.run_grid_method("selectAll")

    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):
        """
        set up a button row
        """
        with ui.row():
            # icons per https://fonts.google.com/icons
            if self.config.editable:
                ui.button("New", icon="add", on_click=self.new_row)
                ui.button("Delete", icon="delete", on_click=self.delete_selected_rows)
            # ui.button("Fit", icon="arrow_range", on_click=self.onSizeColumnsToFit)
            ui.button(
                "All",
                icon="select_all",
                on_click=self.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

required
Source code in ngwidgets/lod_grid.py
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 __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 = {}
    try:
        if self.config.with_buttons:
            self.setup_button_row()
        # 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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
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()

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
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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_row_data()

get the complete row data

Source code in ngwidgets/lod_grid.py
270
271
272
273
274
275
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
227
228
229
230
231
232
233
234
235
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
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_rows() async

get the currently selected rows

Source code in ngwidgets/lod_grid.py
365
366
367
368
369
370
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
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 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.
    """
    if self.config.debug:
        # Print a stack trace to stderr
        print("Exception caught in ListOfDictsGrid:", file=sys.stderr)
        traceback.print_exc()
        # Optionally, re-raise the exception for further debugging.
        raise ex
    elif self.config.exception_callback:
        self.config.exception_callback(ex)
    else:
        # If not in debug mode, notify the user with a general error message.
        # Ensure that ui.notify or a similar method is available and properly configured.
        ui.notify(
            f"Unhandled exception {str(ex)} occurred in ListOfDictsGrid",
            type="error",
        )

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

required
Source code in ngwidgets/lod_grid.py
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
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
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
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
295
296
297
298
299
300
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
372
373
374
375
376
def select_all_rows(self):
    """
    select all my ag_grid rows
    """
    self.ag_grid.run_grid_method("selectAll")

setDefaultColDef()

set the default column definitions

Source code in ngwidgets/lod_grid.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
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
157
158
159
160
161
162
163
164
165
166
167
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
169
170
171
172
173
174
175
176
177
178
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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()

set up a button row

Source code in ngwidgets/lod_grid.py
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
def setup_button_row(self):
    """
    set up a button row
    """
    with ui.row():
        # icons per https://fonts.google.com/icons
        if self.config.editable:
            ui.button("New", icon="add", on_click=self.new_row)
            ui.button("Delete", icon="delete", on_click=self.delete_selected_rows)
        # ui.button("Fit", icon="arrow_range", on_click=self.onSizeColumnsToFit)
        ui.button(
            "All",
            icon="select_all",
            on_click=self.select_all_rows,
        )

update()

update my aggrid

Source code in ngwidgets/lod_grid.py
358
359
360
361
362
363
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
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 the index based on the given key column

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
def update_index(self, lenient: bool = False):
    """
    update the index based on the given key column
    """
    self.lod_index = {}
    if self.lod:
        for row_index, row in enumerate(self.lod):
            if self.config.key_col in row:
                key_value = row[self.config.key_col]
                self.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

log_view

Created on 2023-11-15

@author: wf

LogElementHandler

Bases: Handler

A logging handler that emits messages to a log element.

Source code in ngwidgets/log_view.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class LogElementHandler(logging.Handler):
    """A logging handler that emits messages to a log element."""

    def __init__(self, element: ui.log, level: int = logging.NOTSET) -> None:
        self.element = element
        super().__init__(level)

    def emit(self, record: logging.LogRecord) -> None:
        try:
            msg = self.format(record)
            self.element.push(msg)
        except (KeyboardInterrupt, SystemExit):
            raise
        except Exception:
            self.handleError(record)

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.open("/")
            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.open("/")
        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

required
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
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 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

required

The progress bar is initially set to invisible and its value to 0.

Source code in ngwidgets/progress.py
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 __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
68
69
70
71
72
73
def reset(self):
    """
    reset
    """
    self.value = 0
    self.progress.value = 0

set_description(desc)

set my description

Source code in ngwidgets/progress.py
75
76
77
78
79
80
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
 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
@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
29
30
31
32
33
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
 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
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
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
    ):
        """
        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.
        """
        if not access_token and default_directory:
            access_token = self._read_access_token(default_directory)
        self.github = Github(access_token)

    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)

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
Source code in ngwidgets/projects.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(
    self, default_directory: str = None, access_token: Optional[str] = None
):
    """
    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.
    """
    if not access_token and default_directory:
        access_token = self._read_access_token(default_directory)
    self.github = Github(access_token)

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
106
107
108
109
110
111
112
113
114
115
116
117
118
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
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
@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))

        if load_from_url:
            components = Components.load_from_yaml_url(self.components_url)
            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: int 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: str 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.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
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
@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
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
@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
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
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))

    if load_from_url:
        components = Components.load_from_yaml_url(self.components_url)
        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
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
@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
266
267
268
269
270
271
272
273
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
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
@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: Path property writable

The default directory for saving and loading projects. Returns: Path: The default directory path.

file_path: 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
391
392
393
394
395
396
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
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
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
398
399
400
401
402
403
404
405
406
407
408
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
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
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
438
439
440
441
442
443
444
445
446
447
448
449
450
451
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
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
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
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
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
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
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
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
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
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
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

    def search_projects(self, term: str, limit: int = None) -> List[Project]:
        """
        Search for packages on PyPI and return them as Project instances.

        Args:
            term (str): The search term.
            limit (int, optional): Maximum number of results to return.

        Returns:
            List[Project]: A list of Project instances representing the search results.
        """
        package_dicts = self.search_packages(term, limit)
        return [Project.from_pypi(pkg) for pkg in package_dicts]

    def get_package_info(self, package_name: str) -> dict:
        """
        Get detailed information about a package from PyPI using urllib.

        Args:
            package_name (str): The name of the package to retrieve information for.

        Returns:
            dict: A dictionary containing package information.

        Raises:
            urllib.error.URLError: If there is an issue with the URL.
            ValueError: If the response status code is not 200.
        """
        url = f"{self.base_url}/{package_name}/json"

        response = urllib.request.urlopen(url)

        if response.getcode() != 200:
            raise ValueError(
                f"Failed to fetch package info for {package_name}. Status code: {response.getcode()}"
            )

        package_data = json.loads(response.read())

        return package_data

    def search_packages(self, term: str, limit: int = None) -> list:
        """Search a package in the pypi repositories and retrieve detailed package information.

        Args:
            term (str): The search term.
            limit (int, optional): Maximum number of results to return.

        Returns:
            List[Dict]: A list of dictionaries containing detailed package information.

                see https://raw.githubusercontent.com/shubhodeep9/pipsearch/master/pipsearch/api.py
        """
        # Constructing a search URL and sending the request
        url = "https://pypi.org/search/?q=" + term
        try:
            response = urllib.request.urlopen(url)
            text = response.read()
        except Exception as e:
            raise e

        soup = BeautifulSoup(text, "html.parser")
        packagestable = soup.find("ul", {"class": "unstyled"})
        # Constructing the result list
        packages = []

        # If no package exists then there is no table displayed hence soup.table will be None
        if packagestable is None:
            return packages

        packagerows: ResultSet[Tag] = packagestable.findAll("li")

        if self.debug:
            print(f"found len{packagerows} package rows")
        if limit:
            selected_rows = packagerows[:limit]
        else:
            selected_rows = packagerows
        for package in selected_rows:
            nameSelector = package.find("span", {"class": "package-snippet__name"})
            if nameSelector is None:
                continue
            name = nameSelector.text

            link = ""
            if package.a is not None:
                href = package.a["href"]
                if isinstance(href, list):
                    href = href[0]
                link = "https://pypi.org" + href

            description = (
                package.find("p", {"class": "package-snippet__description"}) or Tag()
            ).text

            version = (
                package.find("span", {"class": "package-snippet__version"}) or Tag()
            ).text
            package_info = self.get_package_info(name)
            package_info["package_url"] = link
            packages.append(package_info)

        # returning the result list back
        return packages

get_package_info(package_name)

Get detailed information about a package from PyPI using urllib.

Parameters:

Name Type Description Default
package_name str

The name of the package to retrieve information for.

required

Returns:

Name Type Description
dict dict

A dictionary containing package information.

Raises:

Type Description
URLError

If there is an issue with the URL.

ValueError

If the response status code is not 200.

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
686
687
688
689
690
691
692
def get_package_info(self, package_name: str) -> dict:
    """
    Get detailed information about a package from PyPI using urllib.

    Args:
        package_name (str): The name of the package to retrieve information for.

    Returns:
        dict: A dictionary containing package information.

    Raises:
        urllib.error.URLError: If there is an issue with the URL.
        ValueError: If the response status code is not 200.
    """
    url = f"{self.base_url}/{package_name}/json"

    response = urllib.request.urlopen(url)

    if response.getcode() != 200:
        raise ValueError(
            f"Failed to fetch package info for {package_name}. Status code: {response.getcode()}"
        )

    package_data = json.loads(response.read())

    return package_data

search_packages(term, limit=None)

Search a package in the pypi repositories and retrieve detailed package information.

Parameters:

Name Type Description Default
term str

The search term.

required
limit int

Maximum number of results to return.

None

Returns:

Type Description
list

List[Dict]: A list of dictionaries containing detailed package information.

see https://raw.githubusercontent.com/shubhodeep9/pipsearch/master/pipsearch/api.py

Source code in ngwidgets/projects.py
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
def search_packages(self, term: str, limit: int = None) -> list:
    """Search a package in the pypi repositories and retrieve detailed package information.

    Args:
        term (str): The search term.
        limit (int, optional): Maximum number of results to return.

    Returns:
        List[Dict]: A list of dictionaries containing detailed package information.

            see https://raw.githubusercontent.com/shubhodeep9/pipsearch/master/pipsearch/api.py
    """
    # Constructing a search URL and sending the request
    url = "https://pypi.org/search/?q=" + term
    try:
        response = urllib.request.urlopen(url)
        text = response.read()
    except Exception as e:
        raise e

    soup = BeautifulSoup(text, "html.parser")
    packagestable = soup.find("ul", {"class": "unstyled"})
    # Constructing the result list
    packages = []

    # If no package exists then there is no table displayed hence soup.table will be None
    if packagestable is None:
        return packages

    packagerows: ResultSet[Tag] = packagestable.findAll("li")

    if self.debug:
        print(f"found len{packagerows} package rows")
    if limit:
        selected_rows = packagerows[:limit]
    else:
        selected_rows = packagerows
    for package in selected_rows:
        nameSelector = package.find("span", {"class": "package-snippet__name"})
        if nameSelector is None:
            continue
        name = nameSelector.text

        link = ""
        if package.a is not None:
            href = package.a["href"]
            if isinstance(href, list):
                href = href[0]
            link = "https://pypi.org" + href

        description = (
            package.find("p", {"class": "package-snippet__description"}) or Tag()
        ).text

        version = (
            package.find("span", {"class": "package-snippet__version"}) or Tag()
        ).text
        package_info = self.get_package_info(name)
        package_info["package_url"] = link
        packages.append(package_info)

    # returning the result list back
    return packages

search_projects(term, limit=None)

Search for packages on PyPI and return them as Project instances.

Parameters:

Name Type Description Default
term str

The search term.

required
limit int

Maximum number of results to return.

None

Returns:

Type Description
List[Project]

List[Project]: A list of Project instances representing the search results.

Source code in ngwidgets/projects.py
653
654
655
656
657
658
659
660
661
662
663
664
665
def search_projects(self, term: str, limit: int = None) -> List[Project]:
    """
    Search for packages on PyPI and return them as Project instances.

    Args:
        term (str): The search term.
        limit (int, optional): Maximum number of results to return.

    Returns:
        List[Project]: A list of Project instances representing the search results.
    """
    package_dicts = self.search_packages(term, limit)
    return [Project.from_pypi(pkg) for pkg in package_dicts]

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)

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
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
@lod_storable
class Version:
    """
    Version handling for nicegui widgets
    """

    name = "ngwidgets"
    version = ngwidgets.__version__
    date = "2023-09-10"
    updated = "2024-07-31"
    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-2024 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
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
class NiceGuiWebserver(object):
    """
    a basic NiceGuiWebserver
    """

    def __init__(self, config: WebserverConfig = None):
        """
        Constructor
        """
        self.debug = False
        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")
        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()

        return await action(*args, **kwargs)

    @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,
            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
113
114
115
116
117
118
119
120
121
122
def __init__(self, config: WebserverConfig = None):
    """
    Constructor
    """
    self.debug = False
    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
219
220
221
222
223
224
225
226
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

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
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
@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
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
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")
    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()

    return await action(*args, **kwargs)

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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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,
        reload=False,
        storage_secret=self.config.storage_secret,
    )

stop()

stop the server

Source code in ngwidgets/webserver.py
228
229
230
231
def stop(self):
    """
    stop the server
    """

WebSolution

the user/client specific web context of a solution

Source code in ngwidgets/webserver.py
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
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.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:
            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 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}")

    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
        """
        version = self.config.version
        if detailed is None:
            detailed = self.config.detailed_menu
        self.config.color_schema.apply()
        with ui.header() as self.header:
            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
239
240
241
242
243
244
245
246
247
248
def __init__(self, webserver: NiceGuiWebserver, client: Client):
    """
    construct a client specific WebSolution
    """
    self.webserver = webserver
    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
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
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
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
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
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
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

required

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
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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:
        ui.button(name, icon=icon_name)
    return link_btn

notify(msg)

call ui.notify with a context

Source code in ngwidgets/webserver.py
250
251
252
253
254
255
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
257
258
259
260
261
262
263
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
265
266
267
268
269
270
271
272
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
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
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

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

required
Note

This method is asynchronous and should be awaited when called.

Source code in ngwidgets/webserver.py
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
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
429
430
431
432
433
434
435
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

required
Source code in ngwidgets/webserver.py
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
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
    """
    version = self.config.version
    if detailed is None:
        detailed = self.config.detailed_menu
    self.config.color_schema.apply()
    with ui.header() as self.header:
        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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
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()

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
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
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
 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
@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)
    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
43
44
45
46
47
48
49
50
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
 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
@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

ThreadedServerRunner

run the Nicegui Server in a thread

Source code in ngwidgets/webserver_test.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
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
        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."""
        # 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
            app.shutdown()
            # 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."
                    )

__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/webserver_test.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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
    self.shutdown_timeout = shutdown_timeout
    self.thread = threading.Thread(target=self._run_server)
    self.thread.daemon = True

start()

Start the web server thread.

Source code in ngwidgets/webserver_test.py
54
55
56
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/webserver_test.py
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
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
        app.shutdown()
        # 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/webserver_test.py
58
59
60
61
62
63
def warn(self, msg: str):
    """
    show the given warning message
    """
    if self.debug:
        print(msg, file=sys.stderr)

WebserverTest

Bases: Basetest

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
 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
class WebserverTest(Basetest):
    """
    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.cmd_parse(
            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.stop()

    def get_response(self, path: str, expected_status_code: int = 200) -> Response:
        """
        Sends a GET request to a specified path and verifies the response status code.

        This method is used for testing purposes to ensure that a GET request to a
        given path returns the expected status code. It returns the response object
        for further inspection or testing if needed.

        Args:
            path (str): The URL path to which the GET request is sent.
            expected_status_code (int): The expected HTTP status code for the response.
                                        Defaults to 200.

        Returns:
            Response: The response object from the GET request.
        """
        response = self.client.get(path)
        self.assertEqual(response.status_code, expected_status_code)
        return response

    def get_html(self, path: str, expected_status_code=200) -> str:
        """
        get the html content for the given path
        """
        response = self.get_response(path, expected_status_code)
        self.assertTrue(response.content is not None)
        html = response.content.decode()
        if self.debug:
            print(html)
        return html

    def getHtml(self, path: str) -> str:
        """
        get the html content for the given path
        """
        html = self.get_html(path)
        return html

    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

    def get_json(self, path: str, expected_status_code: int = 200) -> Any:
        """
        Sends a GET request to a specified path, verifies the response status code,
        and returns the JSON content of the response.

        This method is useful for testing API endpoints that return JSON data.
        It ensures that the request to a given path returns the expected status code
        and then parses and returns the JSON response.

        Args:
            path (str): The URL path to which the GET request is sent.
            expected_status_code (int): The expected HTTP status code for the response.
                                        Defaults to 200.

        Returns:
            Any: The parsed JSON data from the response.

        Raises:
            AssertionError: If the response status code does not match the expected status code.
            JSONDecodeError: If the response body cannot be parsed as JSON.
        """
        response = self.get_response(path, expected_status_code)
        try:
            json_data = response.json()
            return json_data
        except json.JSONDecodeError as e:
            self.fail(
                f"Failed to decode JSON for request {path} from response: {str(e)}"
            )

getHtml(path)

get the html content for the given path

Source code in ngwidgets/webserver_test.py
193
194
195
196
197
198
def getHtml(self, path: str) -> str:
    """
    get the html content for the given path
    """
    html = self.get_html(path)
    return html

get_html(path, expected_status_code=200)

get the html content for the given path

Source code in ngwidgets/webserver_test.py
182
183
184
185
186
187
188
189
190
191
def get_html(self, path: str, expected_status_code=200) -> str:
    """
    get the html content for the given path
    """
    response = self.get_response(path, expected_status_code)
    self.assertTrue(response.content is not None)
    html = response.content.decode()
    if self.debug:
        print(html)
    return html

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
200
201
202
203
204
205
206
207
208
209
210
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_json(path, expected_status_code=200)

Sends a GET request to a specified path, verifies the response status code, and returns the JSON content of the response.

This method is useful for testing API endpoints that return JSON data. It ensures that the request to a given path returns the expected status code and then parses and returns the JSON response.

Parameters:

Name Type Description Default
path str

The URL path to which the GET request is sent.

required
expected_status_code int

The expected HTTP status code for the response. Defaults to 200.

200

Returns:

Name Type Description
Any Any

The parsed JSON data from the response.

Raises:

Type Description
AssertionError

If the response status code does not match the expected status code.

JSONDecodeError

If the response body cannot be parsed as JSON.

Source code in ngwidgets/webserver_test.py
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
def get_json(self, path: str, expected_status_code: int = 200) -> Any:
    """
    Sends a GET request to a specified path, verifies the response status code,
    and returns the JSON content of the response.

    This method is useful for testing API endpoints that return JSON data.
    It ensures that the request to a given path returns the expected status code
    and then parses and returns the JSON response.

    Args:
        path (str): The URL path to which the GET request is sent.
        expected_status_code (int): The expected HTTP status code for the response.
                                    Defaults to 200.

    Returns:
        Any: The parsed JSON data from the response.

    Raises:
        AssertionError: If the response status code does not match the expected status code.
        JSONDecodeError: If the response body cannot be parsed as JSON.
    """
    response = self.get_response(path, expected_status_code)
    try:
        json_data = response.json()
        return json_data
    except json.JSONDecodeError as e:
        self.fail(
            f"Failed to decode JSON for request {path} from response: {str(e)}"
        )

get_response(path, expected_status_code=200)

Sends a GET request to a specified path and verifies the response status code.

This method is used for testing purposes to ensure that a GET request to a given path returns the expected status code. It returns the response object for further inspection or testing if needed.

Parameters:

Name Type Description Default
path str

The URL path to which the GET request is sent.

required
expected_status_code int

The expected HTTP status code for the response. Defaults to 200.

200

Returns:

Name Type Description
Response Response

The response object from the GET request.

Source code in ngwidgets/webserver_test.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def get_response(self, path: str, expected_status_code: int = 200) -> Response:
    """
    Sends a GET request to a specified path and verifies the response status code.

    This method is used for testing purposes to ensure that a GET request to a
    given path returns the expected status code. It returns the response object
    for further inspection or testing if needed.

    Args:
        path (str): The URL path to which the GET request is sent.
        expected_status_code (int): The expected HTTP status code for the response.
                                    Defaults to 200.

    Returns:
        Response: The response object from the GET request.
    """
    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
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 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.cmd_parse(
        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
154
155
156
157
158
159
160
def tearDown(self):
    """
    tear Down everything
    """
    super().tearDown()
    # Stop the server using the ThreadedServerRunner
    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
 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
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
class NiceGuiWidgetsDemo(InputWebSolution):
    """
    Demonstration Solution
    """

    def __init__(self, webserver: "NiceGuiWebserver", 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

    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} clicked")

        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} clicked")

        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_click=foreground_no_slot)
                ui.button("background no slot", on_click=background_no_slot)
                ui.button("foreground with slot", on_click=foreground_with_slot)
                ui.button("background with slot", on_click=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("cellClicked", 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_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 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",
                "HideShow Demo": "/hideshow",
                "Lang": "/langs",
                "ListOfDictsGrid": "/grid",
                "Tristate Demo": "/tristate",
                "pdfviewer": "/pdfviewer",
                "Progressbar": "/progress",
            }

            # 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
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(self, webserver: "NiceGuiWebserver", 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

home() async

provide the main content page

Source code in ngwidgets/widgets_demo.py
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
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",
            "HideShow Demo": "/hideshow",
            "Lang": "/langs",
            "ListOfDictsGrid": "/grid",
            "Tristate Demo": "/tristate",
            "pdfviewer": "/pdfviewer",
            "Progressbar": "/progress",
        }

        # 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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
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
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
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
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
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
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
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
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
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_grid() async

show the lod grid

Source code in ngwidgets/widgets_demo.py
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
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("cellClicked", 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
492
493
494
495
496
497
498
499
500
501
502
503
504
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_issue_1786() async

https://github.com/zauberzeug/nicegui/discussions/1786

Source code in ngwidgets/widgets_demo.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
async def show_issue_1786(self):
    """
    https://github.com/zauberzeug/nicegui/discussions/1786
    """

    def foreground_no_slot(event):
        ui.notify(f"{event.sender.text} clicked")

    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} clicked")

    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_click=foreground_no_slot)
            ui.button("background no slot", on_click=background_no_slot)
            ui.button("foreground with slot", on_click=foreground_with_slot)
            ui.button("background with slot", on_click=background_with_slot)

    await self.setup_content_div(show)

show_langs() async

show languages selection

Source code in ngwidgets/widgets_demo.py
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
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_tristate_demo() async

Demonstrate the Tristate project.

Source code in ngwidgets/widgets_demo.py
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
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
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
class NiceGuiWidgetsDemoWebserver(InputWebserver):
    """
    webserver to demonstrate ngwidgets capabilities
    """

    @classmethod
    def get_config(cls) -> WebserverConfig:
        copy_right = "(c)2023-2024 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())
        # 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"
        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("/langs")
        async def show_langs(client: Client):
            return await self.page(client, NiceGuiWidgetsDemo.show_langs)

        @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("/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
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
def __init__(self):
    """
    Constructor
    """
    InputWebserver.__init__(self, config=NiceGuiWidgetsDemoWebserver.get_config())
    # 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"
    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("/langs")
    async def show_langs(client: Client):
        return await self.page(client, NiceGuiWidgetsDemo.show_langs)

    @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("/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