shellac-webview 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
shellac/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .window import Window
2
+ from .enums import Browser
3
+ from .models import WindowConfig, Event
4
+
5
+ __all__ = ["Window", "Browser", "WindowConfig", "Event"]
shellac/enums.py ADDED
@@ -0,0 +1,11 @@
1
+ from enum import IntEnum
2
+
3
+ class Browser(IntEnum):
4
+ NoBrowser = 0
5
+ AnyBrowser = 1
6
+ Chrome = 2
7
+ Firefox = 3
8
+ Edge = 4
9
+ Chromium = 6
10
+ Brave = 8
11
+ Vivaldi = 9
shellac/launcher.py ADDED
@@ -0,0 +1,110 @@
1
+ import platform
2
+ import shutil
3
+ import tempfile
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from selenium import webdriver
8
+ from selenium.webdriver.chrome.service import Service as ChromeService
9
+ from selenium.webdriver.firefox.service import Service as FirefoxService
10
+ from selenium.webdriver.edge.service import Service as EdgeService
11
+
12
+ from .enums import Browser
13
+ from .models import WindowConfig
14
+
15
+ class BrowserLauncher:
16
+ @staticmethod
17
+ def get_path(browser: Browser) -> Optional[str]:
18
+ system = platform.system()
19
+ if system == "Windows":
20
+ import winreg
21
+ reg_paths = {
22
+ Browser.Chrome: r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
23
+ Browser.Edge: r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\msedge.exe",
24
+ Browser.Firefox: r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\firefox.exe",
25
+ }
26
+ if browser in reg_paths:
27
+ for hive in [winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER]:
28
+ try:
29
+ with winreg.OpenKey(hive, reg_paths[browser]) as key:
30
+ return winreg.QueryValue(key, None)
31
+ except FileNotFoundError: continue
32
+
33
+ names = {Browser.Chrome: "google-chrome", Browser.Firefox: "firefox", Browser.Edge: "microsoft-edge"}
34
+ suffix = ".exe" if system == "Windows" else ""
35
+ return shutil.which(names.get(browser, "") + suffix) or shutil.which(browser.name.lower() + suffix)
36
+
37
+ @staticmethod
38
+ def prepare_firefox_profile(config: WindowConfig) -> str:
39
+ temp_dir = tempfile.mkdtemp(prefix="webui_ff_")
40
+ chrome_dir = Path(temp_dir) / "chrome"
41
+ chrome_dir.mkdir()
42
+
43
+ prefs = {
44
+ "toolkit.legacyUserProfileCustomizations.stylesheets": "true",
45
+ "browser.tabs.inTitlebar": "0",
46
+ "browser.shell.checkDefaultBrowser": "false",
47
+ "browser.startup.page": "0",
48
+ "browser.tabs.warnOnClose": "false",
49
+ "security.csp.enable": "false",
50
+ "security.mixed_content.block_active_content": "false",
51
+ "network.websocket.allowInsecureFromHttp": "true",
52
+ "devtools.chrome.enabled": "true"
53
+ }
54
+
55
+ with open(Path(temp_dir) / "prefs.js", "w") as f:
56
+ for k, v in prefs.items():
57
+ f.write(f'user_pref("{k}", {v});\n')
58
+
59
+ if config.hide_controls:
60
+ with open(chrome_dir / "userChrome.css", "w") as f:
61
+ f.write("""
62
+ @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
63
+ #nav-bar, #TabsToolbar, #PersonalToolbar, #sidebar-box, #urlbar-container {
64
+ visibility: collapse !important;
65
+ }
66
+ """)
67
+ return temp_dir
68
+
69
+ @classmethod
70
+ def create_driver(cls, browser: Browser, url: str, config: WindowConfig) -> webdriver.Remote:
71
+ path = cls.get_path(browser)
72
+
73
+ if browser in [Browser.Chrome, Browser.Edge, Browser.Chromium, Browser.Brave, Browser.Vivaldi]:
74
+ is_edge = browser == Browser.Edge
75
+ options = webdriver.EdgeOptions() if is_edge else webdriver.ChromeOptions()
76
+ if path: options.binary_location = path
77
+
78
+ if config.hide_controls:
79
+ options.add_argument(f"--app={url}")
80
+
81
+ options.add_argument("--disable-web-security")
82
+ options.add_argument("--allow-running-insecure-content")
83
+ options.add_argument("--disable-site-isolation-trials")
84
+ options.add_argument(f"--user-data-dir={tempfile.mkdtemp()}")
85
+
86
+ options.add_argument(f"--window-size={config.width},{config.height}")
87
+ options.add_experimental_option("excludeSwitches", ["enable-automation"])
88
+
89
+ return (webdriver.Edge(options=options) if is_edge else webdriver.Chrome(options=options))
90
+
91
+ elif browser == Browser.Firefox:
92
+ profile_path = cls.prepare_firefox_profile(config)
93
+
94
+ options = webdriver.FirefoxOptions()
95
+ if path: options.binary_location = path
96
+
97
+ options.add_argument("-profile")
98
+ options.add_argument(profile_path)
99
+
100
+ if config.kiosk:
101
+ options.add_argument("--kiosk")
102
+
103
+ options.set_preference("security.csp.enable", False)
104
+
105
+ service = FirefoxService()
106
+ driver = webdriver.Firefox(service=service, options=options)
107
+ driver.set_window_size(config.width, config.height)
108
+ return driver
109
+
110
+ raise ValueError(f"Unsupported browser: {browser}")
shellac/models.py ADDED
@@ -0,0 +1,25 @@
1
+ from typing import Any, List
2
+ from pydantic import BaseModel, Field, ConfigDict
3
+
4
+ class WindowConfig(BaseModel):
5
+ width: int = 1000
6
+ height: int = 800
7
+ hide_controls: bool = True
8
+ kiosk: bool = False
9
+
10
+ class Event(BaseModel):
11
+ model_config = ConfigDict(arbitrary_types_allowed=True)
12
+ window: Any
13
+ element: str = ""
14
+ data: List[Any] = Field(default_factory=list)
15
+
16
+ def get_string(self, index: int = 0) -> str:
17
+ return str(self.data[index]) if index < len(self.data) else ""
18
+
19
+ def get_int(self, index: int = 0) -> int:
20
+ try: return int(self.data[index])
21
+ except (IndexError, ValueError): return 0
22
+
23
+ def get_dict(self, index: int = 0) -> dict:
24
+ try: return dict(self.data[index])
25
+ except (IndexError, ValueError, TypeError): return {}
shellac/window.py ADDED
@@ -0,0 +1,297 @@
1
+ import inspect
2
+ import json
3
+ import os
4
+ import socket
5
+ import threading
6
+ import time
7
+ from typing import Any, Callable, Dict, Optional, Union
8
+
9
+ import uvicorn
10
+ from fastapi import FastAPI
11
+ from fastapi.responses import HTMLResponse
12
+ from selenium import webdriver
13
+ from selenium.common.exceptions import WebDriverException, NoSuchWindowException
14
+
15
+ # Note: These imports assume the structure of your package
16
+ from .enums import Browser
17
+ from .models import WindowConfig, Event
18
+ from .launcher import BrowserLauncher
19
+
20
+
21
+ class Window:
22
+ """
23
+ Represents a WebUI window instance that manages a FastAPI backend
24
+ and a Selenium-controlled browser frontend.
25
+ """
26
+
27
+ def __init__(self):
28
+ """Initializes a new Window instance with default configurations."""
29
+ self.config = WindowConfig()
30
+ self.port = self._get_free_port()
31
+ self.app = FastAPI()
32
+ self.bindings: Dict[str, Callable] = {}
33
+ self._html_content: Optional[str] = None
34
+ self.driver: Optional[webdriver.Remote] = None
35
+ self._running = False
36
+ self._setup_routes()
37
+
38
+ def _get_free_port(self) -> int:
39
+ """Finds an available TCP port on the localhost."""
40
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
41
+ s.bind(('', 0))
42
+ return s.getsockname()[1]
43
+
44
+ def _get_bridge_js(self) -> str:
45
+ """Returns the JavaScript bridge code for Python-JS communication."""
46
+ return """
47
+ if (typeof window.webui === 'undefined') {
48
+ window._webui_queue = [];
49
+ window._webui_promises = {};
50
+ window._webui_call_id = 0;
51
+ window.webui = {
52
+ call: (fn, ...args) => {
53
+ return new Promise((resolve, reject) => {
54
+ const id = window._webui_call_id++;
55
+ window._webui_promises[id] = resolve;
56
+ window._webui_queue.push({fn: fn, args: args, id: id});
57
+ });
58
+ }
59
+ };
60
+ window._webui_resolve = (id, result) => {
61
+ if (window._webui_promises[id]) {
62
+ window._webui_promises[id](result);
63
+ delete window._webui_promises[id];
64
+ }
65
+ };
66
+ }
67
+ """
68
+
69
+ def _setup_routes(self):
70
+ """Configures the internal FastAPI routes."""
71
+ @self.app.get("/")
72
+ async def index():
73
+ content = self._html_content or "<html><body>No Content</body></html>"
74
+ bridge = f"<script>{self._get_bridge_js()}</script>"
75
+ if "<head>" in content:
76
+ return HTMLResponse(content.replace("<head>", f"<head>{bridge}"))
77
+ return HTMLResponse(bridge + content)
78
+
79
+ def _bind_target(self, target: Any, prefix: str = "", exact_name: bool = False):
80
+ """Internal helper to map functions or class methods to the registry."""
81
+ if inspect.isfunction(target) or inspect.ismethod(target):
82
+ name = prefix if (exact_name and prefix) else (f"{prefix}.{target.__name__}" if prefix else target.__name__)
83
+ self.bindings[name] = target
84
+ else:
85
+ if inspect.isclass(target):
86
+ try:
87
+ obj = target()
88
+ except TypeError as e:
89
+ raise ValueError(f"Cannot auto-instantiate class '{target.__name__}'.") from e
90
+ else:
91
+ obj = target
92
+
93
+ for name in dir(obj):
94
+ if not name.startswith('_'):
95
+ attr = getattr(obj, name)
96
+ if callable(attr):
97
+ bind_name = f"{prefix}.{name}" if prefix else name
98
+ self.bindings[bind_name] = attr
99
+
100
+ def bind(self, name_or_target: Union[str, Any] = None, target: Optional[Any] = None):
101
+ """
102
+ Binds a Python function, class, or instance to be callable from JavaScript.
103
+
104
+ This method can be used as a decorator or as a direct function call.
105
+
106
+ Args:
107
+ name_or_target (Union[str, Any], optional): The name/prefix for the binding
108
+ or the target itself if no name is provided.
109
+ target (Any, optional): The function or class instance to bind (only used
110
+ if name_or_target is a string).
111
+
112
+ Returns:
113
+ The original target or a decorator function.
114
+
115
+ Examples:
116
+ >>> win.bind("say_hello", lambda e: "Hello!")
117
+ >>> @win.bind("math")
118
+ ... class Math:
119
+ ... def add(self, e): return e.data[0] + e.data[1]
120
+ """
121
+ if isinstance(name_or_target, str) and target is not None:
122
+ is_func = inspect.isfunction(target) or inspect.ismethod(target)
123
+ self._bind_target(target, prefix=name_or_target, exact_name=is_func)
124
+ return target
125
+
126
+ if isinstance(name_or_target, str) and target is None:
127
+ def decorator(decor_target: Any):
128
+ is_func = inspect.isfunction(decor_target) or inspect.ismethod(decor_target)
129
+ self._bind_target(decor_target, prefix=name_or_target, exact_name=is_func)
130
+ return decor_target
131
+ return decorator
132
+
133
+ if name_or_target is not None:
134
+ self._bind_target(name_or_target, exact_name=False)
135
+ return name_or_target
136
+
137
+ return self
138
+
139
+ def navigate(self, url_or_html: str):
140
+ """
141
+ Navigates the current window to a new URL, a local file, or raw HTML content.
142
+
143
+ Args:
144
+ url_or_html (str): A web URL (http://...), a path to a .html file,
145
+ or a raw HTML string.
146
+ """
147
+ if not self.driver:
148
+ return
149
+
150
+ is_url = url_or_html.startswith(('http://', 'https://', 'file://'))
151
+ if is_url:
152
+ self.driver.get(url_or_html)
153
+ else:
154
+ try:
155
+ if len(url_or_html) < 1000 and os.path.exists(url_or_html):
156
+ with open(url_or_html, 'r', encoding='utf-8') as f:
157
+ self._html_content = f.read()
158
+ else:
159
+ self._html_content = url_or_html
160
+ except OSError:
161
+ self._html_content = url_or_html
162
+
163
+ self.driver.get(f"http://127.0.0.1:{self.port}/")
164
+
165
+ def run_js(self, script: str) -> Any:
166
+ """
167
+ Executes synchronous JavaScript code in the browser.
168
+
169
+ Args:
170
+ script (str): The JavaScript code to execute.
171
+
172
+ Returns:
173
+ Any: The result returned by the JavaScript execution.
174
+ """
175
+ if self.driver:
176
+ try: return self.driver.execute_script(script)
177
+ except Exception as e: print(f"[WebUI] JS Execution Error: {e}")
178
+ return None
179
+
180
+ def close(self):
181
+ """Closes the browser window and shuts down the backend server."""
182
+ self._running = False
183
+ if self.driver:
184
+ try: self.driver.quit()
185
+ except Exception: pass
186
+ self.driver = None
187
+
188
+ def set_size(self, width: int, height: int):
189
+ """
190
+ Resizes the browser window.
191
+
192
+ Args:
193
+ width (int): Target width in pixels.
194
+ height (int): Target height in pixels.
195
+ """
196
+ self.config.width = width
197
+ self.config.height = height
198
+ if self.driver: self.driver.set_window_size(width, height)
199
+
200
+ def set_title(self, title: str):
201
+ """
202
+ Updates the browser window title.
203
+
204
+ Args:
205
+ title (str): The new title string.
206
+ """
207
+ self.run_js(f"document.title = {json.dumps(title)};")
208
+
209
+ def is_running(self) -> bool:
210
+ """
211
+ Checks if the window and backend are currently running.
212
+
213
+ Returns:
214
+ bool: True if running, False otherwise.
215
+ """
216
+ return self._running
217
+
218
+ def _bridge_monitor(self):
219
+ """Internal background thread that polls the JS bridge for incoming calls."""
220
+ while self._running:
221
+ if self.driver:
222
+ try:
223
+ exists = self.driver.execute_script("return (typeof window.webui !== 'undefined' && typeof window._webui_resolve !== 'undefined');")
224
+ if not exists:
225
+ self.driver.execute_script(self._get_bridge_js())
226
+
227
+ events = self.driver.execute_script("""
228
+ var e = window._webui_queue;
229
+ window._webui_queue = [];
230
+ return e;
231
+ """)
232
+
233
+ for event_data in (events or []):
234
+ fn = event_data.get('fn')
235
+ call_id = event_data.get('id')
236
+ if fn in self.bindings:
237
+ event = Event(window=self, element=fn, data=event_data.get('args', []))
238
+ cb = self.bindings[fn]
239
+ result = cb(event)
240
+ result_json = json.dumps(result)
241
+ self.driver.execute_script(f"window._webui_resolve({call_id}, {result_json});")
242
+
243
+ except Exception:
244
+ pass
245
+ time.sleep(0.05)
246
+
247
+ def show(self, content: str, browser: Browser = Browser.AnyBrowser):
248
+ """
249
+ Starts the backend server and launches the browser window.
250
+
251
+ Args:
252
+ content (str): The URL, HTML file path, or HTML string to display.
253
+ browser (Browser): The browser engine to use (defaults to AnyBrowser).
254
+ """
255
+ self._running = True
256
+ is_url = content.startswith(('http://', 'https://', 'file://'))
257
+ if not is_url:
258
+ try:
259
+ if len(content) < 1000 and os.path.exists(content):
260
+ with open(content, 'r', encoding='utf-8') as f: self._html_content = f.read()
261
+ else: self._html_content = content
262
+ except OSError:
263
+ self._html_content = content
264
+ url = f"http://127.0.0.1:{self.port}/"
265
+ else:
266
+ url = content
267
+
268
+ threading.Thread(target=uvicorn.run, args=(self.app,),
269
+ kwargs={"host": "127.0.0.1", "port": self.port, "log_level": "error"},
270
+ daemon=True).start()
271
+
272
+ target = browser
273
+ if target == Browser.AnyBrowser:
274
+ for b in [Browser.Chrome, Browser.Edge, Browser.Firefox]:
275
+ if BrowserLauncher.get_path(b):
276
+ target = b
277
+ break
278
+
279
+ self.driver = BrowserLauncher.create_driver(target, url, self.config)
280
+ self.driver.get(url)
281
+ threading.Thread(target=self._bridge_monitor, daemon=True).start()
282
+
283
+ def wait(self):
284
+ """
285
+ Blocks the main thread until the browser window is closed.
286
+ """
287
+ try:
288
+ while self._running:
289
+ if self.driver:
290
+ try:
291
+ _ = self.driver.window_handles
292
+ except (NoSuchWindowException, WebDriverException):
293
+ break
294
+ time.sleep(1)
295
+ except KeyboardInterrupt: pass
296
+ finally:
297
+ self.close()
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: shellac-webview
3
+ Version: 1.0.1
4
+ Author-email: Sharkow1743 <Sharkow1743@users.noreply.github.com>
5
+ Project-URL: Homepage, https://github.com/Sharkow1743/shellac-webview
6
+ Project-URL: Bug Tracker, https://github.com/Sharkow1743/shellac-webview/issues
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: fastapi
10
+ Requires-Dist: uvicorn
11
+ Requires-Dist: pydantic
12
+ Requires-Dist: selenium
13
+
14
+ # Shellac Webview
15
+
16
+ A Selenium wrapper for building desktop applications using web technologies. It uses FastAPI for backend communication and Selenium for browser automation.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install shellac-webview
22
+ ```
23
+
24
+ ## Basic Usage
25
+
26
+ ```python
27
+ from shellac import Window
28
+
29
+ win = Window()
30
+
31
+ @win.bind
32
+ def my_function(event):
33
+ return "Data from Python"
34
+
35
+ html = """
36
+ <button onclick="run()">Run</button>
37
+ <script>
38
+ async function run() {
39
+ const result = await webui.call("my_function");
40
+ console.log(result);
41
+ }
42
+ </script>
43
+ """
44
+
45
+ win.show(html)
46
+ win.wait()
47
+ ```
48
+
49
+ ## Binding
50
+
51
+ You can bind functions in python to be called from js using `webui.call("func_name")`
52
+
53
+ ### Function Binding
54
+ You can bind a specific function to a name.
55
+ ```python
56
+ def my_function(event):
57
+ return f"Hello, {event.data[0]}!"
58
+
59
+ win.bind("greet", my_function)
60
+ ```
61
+ **JS Usage:**
62
+ ```javascript
63
+ const response = await webui.call("greet", "Alice");
64
+ console.log(response); // "Hello, Alice!"
65
+ ```
66
+
67
+ ### Decorator Binding
68
+ Use the `@bind` decorator for a cleaner syntax.
69
+ ```python
70
+ @win.bind
71
+ def calculate(event):
72
+ return event.data[0] * 2
73
+ ```
74
+ **JS Usage:** `webui.call("calculate", 21);`
75
+
76
+ ### Class/Namespace Binding
77
+ You can bind an entire class or instance. All public methods (not starting with `_`) will be available in JavaScript.
78
+ ```python
79
+ @win.bind("db")
80
+ class Database:
81
+ def get_user(self, event):
82
+ return {"id": event.data[0], "name": "John Doe"}
83
+ ```
84
+ or with
85
+ ```python
86
+ win.bind("db", Database())
87
+ ```
88
+ **JS Usage:**
89
+ ```javascript
90
+ const user = await webui.call("db.get_user", 1);
91
+ ```
92
+
93
+ ### Class Mapping
94
+ If you bind a class without a prefix, its methods are mapped to the top-level.
95
+ ```python
96
+ class API:
97
+ def status(self, event):
98
+ return "Online"
99
+
100
+ win.bind(API)
101
+ ```
102
+ **JS Usage:** `webui.call("status");`
103
+
104
+ ### The `Event` Object
105
+ Every bound Python function receives an `Event` object as its first argument.
106
+ - `event.window`: The `Window` instance.
107
+ - `event.data`: A list of arguments passed from JavaScript.
108
+ - `event.element`: The name of the function called.
109
+
110
+ ## Window Configuration
111
+
112
+ You can configure the window size and behavior before calling `show()`.
113
+
114
+ ```python
115
+ win = Window()
116
+ win.config.width = 1200
117
+ win.config.height = 900
118
+ win.config.hide_controls = True # Hides address bar/tabs (App Mode)
119
+ win.config.kiosk = False # Fullscreen mode
120
+ ```
121
+
122
+ ## Browser Selection
123
+
124
+ By default, the library looks for Chrome, Edge, or Firefox. You can force a specific browser:
125
+
126
+ ```python
127
+ from webui import Browser
128
+
129
+ win.show("index.html", browser=Browser.Firefox)
130
+ ```
131
+
132
+ Available options: `Chrome`, `Firefox`, `Edge`, `Chromium`, `Brave`, `Vivaldi`.
133
+
134
+ ## Other functions
135
+
136
+ Once the window is running, you can navigate to new pages or execute JavaScript from Python.
137
+
138
+ ```python
139
+ # Navigate to a new URL
140
+ win.navigate("https://google.com")
141
+
142
+ # Execute JS directly
143
+ win.run_js("alert('Hello from Python!')")
144
+
145
+ # Change title
146
+ win.set_title("New Title")
147
+ ```
@@ -0,0 +1,9 @@
1
+ shellac/__init__.py,sha256=T_0tUvsKypt1skVvX3Nu17VTqSjAKdZZdVOdhhsOR4g,151
2
+ shellac/enums.py,sha256=ak5X3bh5N3T0tF2uUFwJN7PZ7aRkjGPcXM_0M2_Xg44,177
3
+ shellac/launcher.py,sha256=p_bktZ8b9bbNxq4kcM_HGkTgSLwj4DPyWjjdS8itSr0,4747
4
+ shellac/models.py,sha256=LVPbDokc1VHphN47WXFQsErSsji6ZYaXZdaTPwQkWkY,815
5
+ shellac/window.py,sha256=KJ8m-GLYR9A2wwbY-Xkt1Ne89oQKdCBsBO7NdwrZuuc,11264
6
+ shellac_webview-1.0.1.dist-info/METADATA,sha256=904PPg72YZEjEWhQRSwApJYq60f4J7pPVwcbVj01iXg,3385
7
+ shellac_webview-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ shellac_webview-1.0.1.dist-info/top_level.txt,sha256=XufVWfhTQ-kO571Rqhkmxx5nMdJnCPuU_UiJLHVF2DU,8
9
+ shellac_webview-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ shellac