File: __init__.py

package info (click to toggle)
python-pywebview 6.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 33,436 kB
  • sloc: python: 10,230; javascript: 3,185; java: 522; cs: 130; sh: 16; makefile: 3
file content (489 lines) | stat: -rw-r--r-- 16,462 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
"""
pywebview is a lightweight cross-platform wrapper around a webview component that allows to display HTML content in its
own dedicated window. Works on Windows, OS X and Linux and compatible with Python 2 and 3.

(C) 2014-2019 Roman Sirokov and contributors
Licensed under BSD license

http://github.com/r0x0r/pywebview/
"""

from __future__ import annotations

import datetime
import enum
import logging
import os
import re
import tempfile
import threading
from collections.abc import Iterable, Mapping
from typing import Any, Callable
from uuid import uuid4

import webview.http as http
from webview.errors import JavascriptException, WebViewException
from webview.event import Event
from webview.guilib import initialize, GUIType
from webview.localization import original_localization
from webview.menu import Menu
from webview.screen import Screen
from webview.util import ImmutableDict, _TOKEN, abspath, is_app, is_local_url
from webview.window import Window

__all__ = (
    # Stuff that's here
    'active_window',
    'start',
    'create_window',
    'token',
    'renderer',
    'screens',
    'settings',
    # From event
    'Event',
    # from util    '
    'JavascriptException',
    'WebViewException',
    # from screen
    'Screen',
    # from window
    'Window',
)


def _setup_logger():
    """Setup logger with console handler and appropriate log level."""

    logger = logging.getLogger('pywebview')

    # Avoid duplicate setup
    if logger.handlers:
        return logger

    # Create and configure handler
    handler = logging.StreamHandler()
    formatter = logging.Formatter('[pywebview] %(message)s')
    handler.setFormatter(formatter)
    logger.addHandler(handler)

    # Set log level from environment variable with validation
    log_level_name = os.environ.get('PYWEBVIEW_LOG', 'INFO').upper()
    try:
        log_level = getattr(logging, log_level_name)
        logger.setLevel(log_level)
    except AttributeError:
        # Fallback to INFO if invalid level specified
        logger.setLevel(logging.INFO)
        logger.warning(f"Invalid log level '{log_level_name}', using INFO instead")

    return logger

logger = _setup_logger()

def OPEN_DIALOG():
    logger.warning("OPEN_DIALOG is deprecated and will be removed in a future version. Use 'FileDialog.OPEN' instead.")
    return 10

def FOLDER_DIALOG():
    logger.warning("FOLDER_DIALOG is deprecated and will be removed in a future version. Use 'FileDialog.FOLDER' instead.")
    return 20

def SAVE_DIALOG():
    logger.warning("SAVE_DIALOG is deprecated and will be removed in a future version. Use 'FileDialog.SAVE' instead.")
    return 30

class FileDialog(enum.IntEnum):
    OPEN = 10
    FOLDER = 20
    SAVE = 30


settings = ImmutableDict({
    'ALLOW_DOWNLOADS': False,
    'ALLOW_FILE_URLS': True,
    'DRAG_REGION_SELECTOR': '.pywebview-drag-region',
    'DEFAULT_HTTP_PORT': 42001,
    'OPEN_EXTERNAL_LINKS_IN_BROWSER': True,
    'OPEN_DEVTOOLS_IN_DEBUG': True,
    'REMOTE_DEBUGGING_PORT': None,
    'IGNORE_SSL_ERRORS': False,
    'SHOW_DEFAULT_MENUS': True,
})


_state = ImmutableDict({
    'debug': False,
    'storage_path': None,
    'private_mode': True,
    'user_agent': None,
    'http_server': False,
    'ssl': False,
    'icon': None,
    'menu': None
})


def DRAG_REGION_SELECTOR():
    logger.warning("DRAG_REGION_SELECTOR is deprecated and will be removed in a future version. Use 'settings[\"DRAG_REGION_SELECTOR\"]' instead.")
    return settings['DRAG_REGION_SELECTOR']


guilib = None

token = _TOKEN
windows: list[Window] = []
renderer: str | None = None


def start(
    func: Callable[..., None] | None = None,
    args: Iterable[Any] | None = None,
    localization: dict[str, str] = {},
    gui: GUIType | None = None,
    debug: bool = False,
    http_server: bool = False,
    http_port: int | None = None,
    user_agent: str | None = None,
    private_mode: bool = True,
    storage_path: str | None = None,
    menu: list[Menu] = [],
    server: type[http.ServerType] = http.BottleServer,
    server_args: dict[Any, Any] = {},
    ssl: bool = False,
    icon: str | None = None,
):
    """
    Start a GUI loop and display previously created windows. This function must
    be called from a main thread.

    :param func: Function to invoke upon starting the GUI loop.
    :param args: Function arguments. Can be either a single value or a tuple of
        values.
    :param localization: A dictionary with localized strings. Default strings
        and their keys are defined in localization.py.
    :param gui: Force a specific GUI. Allowed values are ``cef``, ``qt``,
        ``gtk``, ``mshtml`` or ``edgechromium`` depending on a platform.
    :param debug: Enable debug mode. Default is False.
    :param http_server: Enable built-in HTTP server. If enabled, local files
        will be served using a local HTTP server on a random port. For each
        window, a separate HTTP server is spawned. This option is ignored for
        non-local URLs.
    :param user_agent: Change user agent string.
    :param private_mode: Enable private mode. In private mode, cookies and local storage are not preserved.
           Default is True.
    :param storage_path: Custom location for cookies and other website data
    :param menu: List of menus to be included in the app menu
    :param server: Server class. Defaults to BottleServer
    :param server_args: Dictionary of arguments to pass through to the server instantiation
    :param ssl: Enable SSL for local HTTP server. Default is False.
    :param icon: Path to the icon file. Supported only on GTK/QT.
    """
    global guilib, renderer

    def _create_children(other_windows):
        if not windows[0].events.shown.wait(10):
            raise WebViewException('Main window failed to load')

        for window in other_windows:
            guilib.create_window(window)

    _state['debug'] = debug
    _state['user_agent'] = user_agent
    _state['http_server'] = http_server
    _state['private_mode'] = private_mode

    if icon:
        _state['icon'] = abspath(icon)

    if storage_path:
        __set_storage_path(storage_path)

    if debug and not os.environ.get('PYWEBVIEW_LOG'):
        logger.setLevel(logging.DEBUG)

    if _state['storage_path'] and _state['private_mode'] and not os.path.exists(_state['storage_path']):
        os.makedirs(_state['storage_path'])

    original_localization.update(localization)

    if threading.current_thread().name != 'MainThread':
        raise WebViewException('pywebview must be run on a main thread.')

    if len(windows) == 0:
        raise WebViewException('You must create a window first before calling this function.')

    guilib = initialize(gui)
    renderer = guilib.renderer

    if ssl:
        if not server_args or 'keyfile' not in server_args or 'certfile' not in server_args:
            # generate SSL certs and tell the windows to use them
            keyfile, certfile = __generate_ssl_cert()
            server_args['keyfile'] = keyfile
            server_args['certfile'] = certfile
        else:
            keyfile = server_args['keyfile']
            certfile = server_args['certfile']
            if not os.path.exists(keyfile):
                raise WebViewException(f'The {keyfile} does not exist.')
            if not os.path.exists(certfile):
                raise WebViewException(f'The {certfile} does not exist.')

        _state['ssl'] = True
    else:
        keyfile, certfile = None, None
        server_args.pop('keyfile', None)
        server_args.pop('certfile', None)

    urls = [w.original_url for w in windows]
    has_local_urls = not not [w.original_url for w in windows if is_local_url(w.original_url)]
    # start the global server if it's not running and we need it
    if (http.global_server is None) and (http_server or has_local_urls):
        if not _state['private_mode'] and not http_port:
            http_port = settings['DEFAULT_HTTP_PORT']
        *_, server = http.start_global_server(
            http_port=http_port, urls=urls, server=server, **server_args
        )

    for window in windows:
        should_initialize = not window._initialize(guilib, server_args=server_args)
        if should_initialize:
            return

    if ssl:
        for window in windows:
            window.gui.add_tls_cert(certfile)

    if len(windows) > 1:
        thread = threading.Thread(target=_create_children, args=(windows[1:],))
        thread.start()

    if func:
        if args is not None:
            if not hasattr(args, '__iter__'):
                args = (args,)
            thread = threading.Thread(target=func, args=args)
        else:
            thread = threading.Thread(target=func)
        thread.start()

    if menu:
        _state['menu'] = menu
        #guilib.set_app_menu(menu)

    guilib.create_window(windows[0])
    # keyfile is deleted by the ServerAdapter right after wrap_socket()
    if certfile:
        os.unlink(certfile)


def create_window(
    title: str,
    url: str | callable | None = None,
    html: str | None = None,
    js_api: Any = None,
    width: int = 800,
    height: int = 600,
    x: int | None = None,
    y: int | None = None,
    screen: Screen = None,
    resizable: bool = True,
    fullscreen: bool = False,
    min_size: tuple[int, int] = (200, 100),
    hidden: bool = False,
    frameless: bool = False,
    easy_drag: bool = True,
    shadow: bool = True,
    focus: bool = True,
    minimized: bool = False,
    maximized: bool = False,
    on_top: bool = False,
    confirm_close: bool = False,
    background_color: str = '#FFFFFF',
    transparent: bool = False,
    text_select: bool = False,
    zoomable: bool = False,
    draggable: bool = False,
    vibrancy: bool = False,
    menu: list[Menu] = [],
    localization: Mapping[str, str] | None = None,
    server: type[http.ServerType] = http.BottleServer,
    http_port: int | None = None,
    server_args: http.ServerArgs = {},
) -> Window | None:
    """
    Create a web view window using a native GUI. The execution blocks after this function is invoked, so other
    program logic must be executed in a separate thread.
    :param title: Window title
    :param url: URL or WSGI/ASGI app to load
    :param html: HTML content to load
    :param width: window width. Default is 800px
    :param height: window height. Default is 600px
    :param screen: Screen to display the window on.
    :param resizable: True if window can be resized, False otherwise. Default is True
    :param fullscreen: True if start in fullscreen mode. Default is False
    :param min_size: a (width, height) tuple that specifies a minimum window size. Default is 200x100
    :param hidden: Whether the window should be hidden.
    :param frameless: Whether the window should have a frame.
    :param easy_drag: Easy window drag mode when window is frameless.
    :param shadow: Whether the window should have a frame border (shadows and Windows rounded edges).
    :param focus: Whether to activate the window when user opens it. Window can be controlled with mouse but keyboard input will go to another (active) window and not this one.
    :param minimized: Display window minimized
    :param maximized: Display window maximized
    :param on_top: Keep window above other windows (required OS: Windows)
    :param confirm_close: Display a window close confirmation dialog. Default is False
    :param background_color: Background color as a hex string that is displayed before the content of webview is loaded. Default is white.
    :param text_select: Allow text selection on page. Default is False.
    :param transparent: Don't draw window background.
    :param menu: List of menus to be included in the window menu
    :param server: Server class. Defaults to BottleServer
    :param server_args: Dictionary of arguments to pass through to the server instantiation
    :return: window object or None if window initialization is cancelled in the window.events.initialized event
    """

    valid_color = r'^#(?:[0-9a-fA-F]{3}){1,2}$'
    if not re.match(valid_color, background_color):
        raise ValueError('{0} is not a valid hex triplet color'.format(background_color))

    uid = 'master' if len(windows) == 0 else 'child_' + uuid4().hex[:8]

    window = Window(
        uid,
        title,
        url,
        html,
        width,
        height,
        x,
        y,
        resizable,
        fullscreen,
        min_size,
        hidden,
        frameless,
        easy_drag,
        shadow,
        focus,
        minimized,
        maximized,
        on_top,
        confirm_close,
        background_color,
        js_api,
        text_select,
        transparent,
        zoomable,
        draggable,
        vibrancy,
        menu,
        localization,
        server=server,
        http_port=http_port,
        server_args=server_args,
        screen=screen,
    )

    windows.append(window)

    # This immediately creates the window only if `start` has already been called
    if threading.current_thread().name != 'MainThread' and guilib:
        if is_app(url) or is_local_url(url) and not server.is_running:
            url_prefix, common_path, server = http.start_server([url], server=server, **server_args)
        else:
            url_prefix, common_path, server = None, None, None

        if not window._initialize(gui=guilib, server=server):
            return
        guilib.create_window(window)

    return window


def __generate_ssl_cert():
    # https://cryptography.io/en/latest/x509/tutorial/#creating-a-self-signed-certificate
    from cryptography import x509
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import rsa
    from cryptography.x509.oid import NameOID

    with tempfile.NamedTemporaryFile(prefix='keyfile_', suffix='.pem', delete=False) as f:
        keyfile = f.name
        key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=2048,
            backend=default_backend()
        )
        key_pem = key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.TraditionalOpenSSL,
            encryption_algorithm=serialization.NoEncryption(),  # BestAvailableEncryption(b"passphrase"),
        )
        f.write(key_pem)

    with tempfile.NamedTemporaryFile(prefix='certfile_', suffix='.pem', delete=False) as f:
        certfile = f.name
        subject = issuer = x509.Name(
            [
                x509.NameAttribute(NameOID.COUNTRY_NAME, 'US'),
                x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, 'California'),
                x509.NameAttribute(NameOID.LOCALITY_NAME, 'San Francisco'),
                x509.NameAttribute(NameOID.ORGANIZATION_NAME, 'pywebview'),
                x509.NameAttribute(NameOID.COMMON_NAME, '127.0.0.1'),
            ]
        )
        cert = (
            x509.CertificateBuilder()
            .subject_name(subject)
            .issuer_name(issuer)
            .public_key(key.public_key())
            .serial_number(x509.random_serial_number())
            .not_valid_before(datetime.datetime.utcnow())
            .not_valid_after(datetime.datetime.utcnow() + datetime.timedelta(days=365))
            .add_extension(
                x509.SubjectAlternativeName([x509.DNSName('localhost')]),
                critical=False,
            )
            .sign(key, hashes.SHA256(), backend=default_backend())
        )
        cert_pem = cert.public_bytes(serialization.Encoding.PEM)
        f.write(cert_pem)

    return keyfile, certfile


def __set_storage_path(storage_path):
    e = WebViewException(f'Storage path {storage_path} is not writable')

    if not os.path.exists(storage_path):
        try:
            os.makedirs(storage_path)
        except OSError:
            raise e
    if not os.access(storage_path, os.W_OK):
        raise e

    _state['storage_path'] = storage_path


def active_window() -> Window | None:
    """
    Get the active window

    :return: window object or None
    """
    if guilib:
        return guilib.get_active_window()
    return None


def screens() -> list[Screen]:
    global renderer, guilib

    if not guilib:
        guilib = initialize()
        renderer = guilib.renderer

    screens = guilib.get_screens()
    return screens