pyrite-web 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pyrite contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrite-web
3
+ Version: 0.1.0
4
+ Summary: Code websites in Python 3, without writing JavaScript or CSS.
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Dynamic: license-file
10
+
11
+ # Pyrite
12
+
13
+ Code websites in Python 3, for JavaScript and CSS haters.
14
+
15
+ Pyrite is a small, dependency-free starting point: compose Python components,
16
+ return a page, and serve HTML with built-in responsive styling. Requires Python 3.10+.
17
+
18
+ ## Run the example
19
+
20
+ ```sh
21
+ python3 example.py
22
+ ```
23
+
24
+ Open http://127.0.0.1:8000. The example is Pyrite's first website: a homepage
25
+ with a code/output comparison, quickstart, expandable FAQ, and a component
26
+ guide at `/guide`. Try `/playground` for a Python-powered calculator.
27
+ Its application source uses only Python components.
28
+ No installation is required when running from this repository.
29
+
30
+ ## Build a page
31
+
32
+ ```python
33
+ from pyrite import App, Column, Heading, Page, Text
34
+
35
+ app = App()
36
+
37
+ @app.page("/")
38
+ def home():
39
+ return Page(
40
+ Column(Heading("Hello, Pyrite."), Text("Just Python.")),
41
+ title="My website",
42
+ )
43
+
44
+ if __name__ == "__main__":
45
+ app.run()
46
+ ```
47
+
48
+ Available components: `Heading`, `Text`, `Column`, `Row`, `Card`, `Link`, and
49
+ `Button`, `Section`, `Nav`, `Code`, and `Details`. Nest components directly.
50
+ `Section(..., id="start")` creates an anchor target. `Code(source, label="Python")`
51
+ displays escaped source. `Details(question, ...)` provides a native expandable answer.
52
+ `Heading` accepts `level=1` through `6`.
53
+ `Link(label, href)` and `Button(label, href)` navigate to another URL.
54
+ Text and HTML attributes are escaped automatically.
55
+
56
+ Register exact paths with `@app.page("/about")`. Handlers take no arguments and
57
+ return a `Page`. Query strings do not affect route matching. `Page` accepts
58
+ `title` and `language`; `app.run` accepts `host` and `port`.
59
+
60
+ ## Current scope
61
+
62
+ This version serves server-rendered pages with native GET forms and JSON POST
63
+ callbacks through `LiveForm` and `Chat`. It does not include server sessions,
64
+ token streaming, live reload, or a production server. The built-in
65
+ HTTP server is for local development. Styling is supplied by Pyrite; applications
66
+ do not need CSS files or JavaScript tooling.
67
+
68
+ ## Styling in Python
69
+
70
+ Edit the root `styles.py` to change the example website's shared theme. Restart
71
+ the server after editing. `Theme` supports six-digit hex colors, `sans`, `serif`,
72
+ or `mono` fonts, page width, corner radius, and spacing. Apply a theme to any page:
73
+
74
+ ```python
75
+ from pyrite import Theme
76
+
77
+ theme = Theme(background="#111820", text="#eef3f8", heading_font="sans")
78
+ page = Page(Heading("Your style."), theme=theme)
79
+ ```
80
+
81
+ ## Forms without JavaScript
82
+
83
+ ```python
84
+ from pyrite import Form, Input, Submit, query
85
+
86
+ @app.page("/hello")
87
+ def hello():
88
+ return Page(Column(
89
+ Form(Input("name", "Your name", value=query("name")), Submit("Say hello")),
90
+ Text(f"Hello, {query('name', 'world')}!"),
91
+ ))
92
+ ```
93
+
94
+ `Slider(name, label, value=5, minimum=1, maximum=10)` and
95
+ `Select(name, label, {"value": "Label"}, value="value")` also work in forms.
96
+ Submission reloads the page; `query` reads request-local URL parameters. GET forms
97
+ are for read-only calculations and searches; their values appear in the URL.
98
+
99
+ ## Try Pyrite in Browser
100
+
101
+ Visit `/try` to edit Python and render a real `Page` without installing Python.
102
+ Assign your result to `page`; do not call `app.run()`. The editor loads Pyodide
103
+ from jsDelivr and runs code in a disposable Web Worker, with a 10-second execution
104
+ limit and an isolated preview. First download can take up to two minutes.
105
+ Code is not executed on the application server. Browser networking follows CORS
106
+ rules; the standard-library HTTP client below is for server-side Python.
107
+
108
+ ## Images and motion
109
+
110
+ ```python
111
+ from pyrite import Image, Motion
112
+ from styles import hero_image
113
+
114
+ banner = Motion(Image(hero_image, "Mountain landscape"), effect="rise")
115
+ ```
116
+
117
+ Choose `rise`, `fade`, or `float`. In `styles.py`, set `motion=False` to disable
118
+ component animations, or change `motion_duration` (100–5000 milliseconds) for
119
+ entrances. Reduced-motion browser preferences disable animations automatically.
120
+ `Theme(background_image="https://example.com/background.jpg")` attaches a page
121
+ background. `Image` accepts relative or HTTP(S) URLs and requires alt text.
122
+ External images load directly from their host; local file serving is not included.
123
+
124
+ ## Requests and changing API data
125
+
126
+ ```python
127
+ from pyrite import requests
128
+
129
+ data = requests.get(
130
+ "https://api.mymemory.translated.net/get",
131
+ params={"q": "Good morning", "langpair": "en|uk"},
132
+ ).json()
133
+
134
+ # For an API you control, with the authentication it requires:
135
+ # requests.post("https://your-api.example/items", json={"name": "New"})
136
+ # requests.put("https://your-api.example/items/1", json={"name": "Changed"})
137
+ # requests.delete("https://your-api.example/items/1")
138
+ ```
139
+
140
+ The dependency-free client supports `get`, `post`, `put`, and `delete`, optional
141
+ headers, JSON bodies, query parameters, and a timeout (8 seconds by default).
142
+ Responses expose `status_code`, `text`, and `json()`. Catch `requests.RequestError`
143
+ for HTTP/network/decoding failures. Responses are limited to 1 MiB. Supply trusted
144
+ URLs in application code; do not forward visitor-provided URLs to this client.
145
+ GET page handlers should only read data. POST actions can make remote writes;
146
+ add application authentication and authorization before exposing private data or
147
+ operations. Pyrite's local development server does not provide authentication.
148
+
149
+ `/translate` demonstrates input, a real free MyMemory request, and updated page
150
+ content. No API key required; availability, translation quality, and quotas depend
151
+ on MyMemory. Submitted text is sent to that service.
152
+
153
+ ## Corners and gradients
154
+
155
+ ```python
156
+ from pyrite import Gradient, Theme
157
+
158
+ theme = Theme(
159
+ radius=20, # Panels, code blocks, images, details
160
+ button_radius=999, # Pill buttons; 0 gives square corners
161
+ input_radius=12, # Inputs and dropdowns
162
+ gradient=Gradient(
163
+ colors=("#101923", "#214a42", "#17283d"),
164
+ angle=120,
165
+ moving=True,
166
+ duration=14, # Seconds per cycle
167
+ ),
168
+ )
169
+ ```
170
+
171
+ `button_radius` and `input_radius` inherit `radius` when omitted. Gradients accept
172
+ 2–8 hex colors; set `moving=False` for a static gradient or `gradient=None` for
173
+ the usual background. A fixed background layer animates independently of page
174
+ length. `motion=False` or the browser's reduced-motion preference stops its movement.
175
+ The gradient layer sits over a background image when both are configured.
176
+
177
+ ## In-page requests and chat
178
+
179
+ ```python
180
+ from pyrite import App, Chat, Page
181
+
182
+ app = App()
183
+
184
+ @app.action("/api/reply")
185
+ def reply(data):
186
+ message = data["message"]
187
+ history = data.get("history", [])
188
+ # Call a model here with pyrite.requests; keep API keys on the server.
189
+ return {"reply": "You said: " + message}
190
+
191
+ @app.page("/")
192
+ def home():
193
+ return Page(Chat(action="/api/reply"))
194
+ ```
195
+
196
+ Visit `/chat` for a working local rule-based chatbot and a live translation form.
197
+ `Chat` sends `{message, history}` and displays `reply` without a page reload.
198
+ History stays in this page's browser memory (last 20 messages), is sent with each
199
+ request, and clears on reload. It is untrusted input; validate it in callbacks.
200
+ This is completed-response chat, not token streaming. The demo uses no AI service.
201
+
202
+ For other interfaces, use `LiveForm(Input(...), Submit(...), action="/api/action")`
203
+ and return `{"text": "Updated result"}` from the action. All field values arrive
204
+ as strings. Pyrite supplies the JavaScript, pending state, errors, 30-second
205
+ client timeout, and safe text rendering. The callback can use `requests` to reach
206
+ external APIs without exposing credentials to the browser. Aborting a request
207
+ does not cancel Python work already running.
208
+
209
+ Actions accept JSON objects up to 16 KiB and reject cross-origin browser requests.
210
+ These checks do not authenticate users. Use application validation and authorization
211
+ for sensitive actions. Live components require JavaScript; ordinary pages and GET
212
+ forms continue working without it. Browser playground previews remain isolated and
213
+ cannot call server actions.
214
+
215
+ ## Tests
216
+
217
+ ```sh
218
+ python3 -m unittest discover -s tests -v
219
+ ```
@@ -0,0 +1,209 @@
1
+ # Pyrite
2
+
3
+ Code websites in Python 3, for JavaScript and CSS haters.
4
+
5
+ Pyrite is a small, dependency-free starting point: compose Python components,
6
+ return a page, and serve HTML with built-in responsive styling. Requires Python 3.10+.
7
+
8
+ ## Run the example
9
+
10
+ ```sh
11
+ python3 example.py
12
+ ```
13
+
14
+ Open http://127.0.0.1:8000. The example is Pyrite's first website: a homepage
15
+ with a code/output comparison, quickstart, expandable FAQ, and a component
16
+ guide at `/guide`. Try `/playground` for a Python-powered calculator.
17
+ Its application source uses only Python components.
18
+ No installation is required when running from this repository.
19
+
20
+ ## Build a page
21
+
22
+ ```python
23
+ from pyrite import App, Column, Heading, Page, Text
24
+
25
+ app = App()
26
+
27
+ @app.page("/")
28
+ def home():
29
+ return Page(
30
+ Column(Heading("Hello, Pyrite."), Text("Just Python.")),
31
+ title="My website",
32
+ )
33
+
34
+ if __name__ == "__main__":
35
+ app.run()
36
+ ```
37
+
38
+ Available components: `Heading`, `Text`, `Column`, `Row`, `Card`, `Link`, and
39
+ `Button`, `Section`, `Nav`, `Code`, and `Details`. Nest components directly.
40
+ `Section(..., id="start")` creates an anchor target. `Code(source, label="Python")`
41
+ displays escaped source. `Details(question, ...)` provides a native expandable answer.
42
+ `Heading` accepts `level=1` through `6`.
43
+ `Link(label, href)` and `Button(label, href)` navigate to another URL.
44
+ Text and HTML attributes are escaped automatically.
45
+
46
+ Register exact paths with `@app.page("/about")`. Handlers take no arguments and
47
+ return a `Page`. Query strings do not affect route matching. `Page` accepts
48
+ `title` and `language`; `app.run` accepts `host` and `port`.
49
+
50
+ ## Current scope
51
+
52
+ This version serves server-rendered pages with native GET forms and JSON POST
53
+ callbacks through `LiveForm` and `Chat`. It does not include server sessions,
54
+ token streaming, live reload, or a production server. The built-in
55
+ HTTP server is for local development. Styling is supplied by Pyrite; applications
56
+ do not need CSS files or JavaScript tooling.
57
+
58
+ ## Styling in Python
59
+
60
+ Edit the root `styles.py` to change the example website's shared theme. Restart
61
+ the server after editing. `Theme` supports six-digit hex colors, `sans`, `serif`,
62
+ or `mono` fonts, page width, corner radius, and spacing. Apply a theme to any page:
63
+
64
+ ```python
65
+ from pyrite import Theme
66
+
67
+ theme = Theme(background="#111820", text="#eef3f8", heading_font="sans")
68
+ page = Page(Heading("Your style."), theme=theme)
69
+ ```
70
+
71
+ ## Forms without JavaScript
72
+
73
+ ```python
74
+ from pyrite import Form, Input, Submit, query
75
+
76
+ @app.page("/hello")
77
+ def hello():
78
+ return Page(Column(
79
+ Form(Input("name", "Your name", value=query("name")), Submit("Say hello")),
80
+ Text(f"Hello, {query('name', 'world')}!"),
81
+ ))
82
+ ```
83
+
84
+ `Slider(name, label, value=5, minimum=1, maximum=10)` and
85
+ `Select(name, label, {"value": "Label"}, value="value")` also work in forms.
86
+ Submission reloads the page; `query` reads request-local URL parameters. GET forms
87
+ are for read-only calculations and searches; their values appear in the URL.
88
+
89
+ ## Try Pyrite in Browser
90
+
91
+ Visit `/try` to edit Python and render a real `Page` without installing Python.
92
+ Assign your result to `page`; do not call `app.run()`. The editor loads Pyodide
93
+ from jsDelivr and runs code in a disposable Web Worker, with a 10-second execution
94
+ limit and an isolated preview. First download can take up to two minutes.
95
+ Code is not executed on the application server. Browser networking follows CORS
96
+ rules; the standard-library HTTP client below is for server-side Python.
97
+
98
+ ## Images and motion
99
+
100
+ ```python
101
+ from pyrite import Image, Motion
102
+ from styles import hero_image
103
+
104
+ banner = Motion(Image(hero_image, "Mountain landscape"), effect="rise")
105
+ ```
106
+
107
+ Choose `rise`, `fade`, or `float`. In `styles.py`, set `motion=False` to disable
108
+ component animations, or change `motion_duration` (100–5000 milliseconds) for
109
+ entrances. Reduced-motion browser preferences disable animations automatically.
110
+ `Theme(background_image="https://example.com/background.jpg")` attaches a page
111
+ background. `Image` accepts relative or HTTP(S) URLs and requires alt text.
112
+ External images load directly from their host; local file serving is not included.
113
+
114
+ ## Requests and changing API data
115
+
116
+ ```python
117
+ from pyrite import requests
118
+
119
+ data = requests.get(
120
+ "https://api.mymemory.translated.net/get",
121
+ params={"q": "Good morning", "langpair": "en|uk"},
122
+ ).json()
123
+
124
+ # For an API you control, with the authentication it requires:
125
+ # requests.post("https://your-api.example/items", json={"name": "New"})
126
+ # requests.put("https://your-api.example/items/1", json={"name": "Changed"})
127
+ # requests.delete("https://your-api.example/items/1")
128
+ ```
129
+
130
+ The dependency-free client supports `get`, `post`, `put`, and `delete`, optional
131
+ headers, JSON bodies, query parameters, and a timeout (8 seconds by default).
132
+ Responses expose `status_code`, `text`, and `json()`. Catch `requests.RequestError`
133
+ for HTTP/network/decoding failures. Responses are limited to 1 MiB. Supply trusted
134
+ URLs in application code; do not forward visitor-provided URLs to this client.
135
+ GET page handlers should only read data. POST actions can make remote writes;
136
+ add application authentication and authorization before exposing private data or
137
+ operations. Pyrite's local development server does not provide authentication.
138
+
139
+ `/translate` demonstrates input, a real free MyMemory request, and updated page
140
+ content. No API key required; availability, translation quality, and quotas depend
141
+ on MyMemory. Submitted text is sent to that service.
142
+
143
+ ## Corners and gradients
144
+
145
+ ```python
146
+ from pyrite import Gradient, Theme
147
+
148
+ theme = Theme(
149
+ radius=20, # Panels, code blocks, images, details
150
+ button_radius=999, # Pill buttons; 0 gives square corners
151
+ input_radius=12, # Inputs and dropdowns
152
+ gradient=Gradient(
153
+ colors=("#101923", "#214a42", "#17283d"),
154
+ angle=120,
155
+ moving=True,
156
+ duration=14, # Seconds per cycle
157
+ ),
158
+ )
159
+ ```
160
+
161
+ `button_radius` and `input_radius` inherit `radius` when omitted. Gradients accept
162
+ 2–8 hex colors; set `moving=False` for a static gradient or `gradient=None` for
163
+ the usual background. A fixed background layer animates independently of page
164
+ length. `motion=False` or the browser's reduced-motion preference stops its movement.
165
+ The gradient layer sits over a background image when both are configured.
166
+
167
+ ## In-page requests and chat
168
+
169
+ ```python
170
+ from pyrite import App, Chat, Page
171
+
172
+ app = App()
173
+
174
+ @app.action("/api/reply")
175
+ def reply(data):
176
+ message = data["message"]
177
+ history = data.get("history", [])
178
+ # Call a model here with pyrite.requests; keep API keys on the server.
179
+ return {"reply": "You said: " + message}
180
+
181
+ @app.page("/")
182
+ def home():
183
+ return Page(Chat(action="/api/reply"))
184
+ ```
185
+
186
+ Visit `/chat` for a working local rule-based chatbot and a live translation form.
187
+ `Chat` sends `{message, history}` and displays `reply` without a page reload.
188
+ History stays in this page's browser memory (last 20 messages), is sent with each
189
+ request, and clears on reload. It is untrusted input; validate it in callbacks.
190
+ This is completed-response chat, not token streaming. The demo uses no AI service.
191
+
192
+ For other interfaces, use `LiveForm(Input(...), Submit(...), action="/api/action")`
193
+ and return `{"text": "Updated result"}` from the action. All field values arrive
194
+ as strings. Pyrite supplies the JavaScript, pending state, errors, 30-second
195
+ client timeout, and safe text rendering. The callback can use `requests` to reach
196
+ external APIs without exposing credentials to the browser. Aborting a request
197
+ does not cancel Python work already running.
198
+
199
+ Actions accept JSON objects up to 16 KiB and reject cross-origin browser requests.
200
+ These checks do not authenticate users. Use application validation and authorization
201
+ for sensitive actions. Live components require JavaScript; ordinary pages and GET
202
+ forms continue working without it. Browser playground previews remain isolated and
203
+ cannot call server actions.
204
+
205
+ ## Tests
206
+
207
+ ```sh
208
+ python3 -m unittest discover -s tests -v
209
+ ```
@@ -0,0 +1,14 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyrite-web"
7
+ version = "0.1.0"
8
+ description = "Code websites in Python 3, without writing JavaScript or CSS."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+
13
+ [tool.setuptools.packages.find]
14
+ include = ["pyrite*"]
@@ -0,0 +1,17 @@
1
+ """Websites in Python, without handwritten JavaScript or CSS."""
2
+
3
+ from .core import App, Button, Card, Code, Column, Details, Heading, Link, Nav, Page, Row, Section, Text
4
+ from .core import Form, Input, Select, Slider, Submit, query
5
+ from .styles import Theme
6
+ from .core import Image, Motion
7
+ from . import requests
8
+ from .browser import BrowserPlayground
9
+ from .styles import Gradient
10
+ from .live import Chat, LiveForm
11
+
12
+ __all__ = ["App", "Button", "Card", "Code", "Column", "Details", "Heading", "Link", "Nav", "Page", "Row", "Section", "Text"]
13
+ __all__ += ["Form", "Input", "Select", "Slider", "Submit", "query"]
14
+ __all__ += ["Theme"]
15
+ __all__ += ["Image", "Motion", "requests"]
16
+ __all__ += ["BrowserPlayground"]
17
+ __all__ += ["Gradient", "Chat", "LiveForm"]
@@ -0,0 +1,105 @@
1
+ """A browser-only Python editor, isolated from the application server."""
2
+ import json
3
+ from html import escape
4
+ from pathlib import Path
5
+
6
+ from .core import Element
7
+
8
+
9
+ STARTER = '''from pyrite import Page, Column, Heading, Text, Card, Theme, Motion
10
+
11
+ theme = Theme(
12
+ background="#111820", text="#eef3f8",
13
+ accent="#baf26b", heading_font="sans",
14
+ )
15
+
16
+ page = Page(
17
+ Motion(
18
+ Heading("I made this in Python."),
19
+ Card(Text("Change the words. Change the theme. Hit Run.")),
20
+ effect="rise",
21
+ ),
22
+ theme=theme,
23
+ )
24
+ '''
25
+
26
+
27
+ class BrowserPlayground(Element):
28
+ """One editor per page. Pyodide runs user code in a disposable Web Worker."""
29
+
30
+ def __init__(self):
31
+ super().__init__("div", ())
32
+
33
+ def render(self):
34
+ # Ship this exact installed framework, rather than a second implementation.
35
+ sources = {path.name: path.read_text() for path in Path(__file__).parent.glob("*.py") if path.name != "browser.py"}
36
+ # The browser doesn't need to import its editor or the server-side HTTP client.
37
+ sources["__init__.py"] = sources["__init__.py"].replace("from .browser import BrowserPlayground", "")
38
+ sources["__init__.py"] = sources["__init__.py"].replace('__all__ += ["BrowserPlayground"]', "")
39
+ payload = json.dumps(sources).replace("<", "\\u003c")
40
+ return '''<div class="column">
41
+ <label for="pyrite-editor">Python source</label>
42
+ <textarea id="pyrite-editor" spellcheck="false">''' + escape(STARTER) + r'''</textarea>
43
+ <div class="row"><button class="button" id="pyrite-run" type="button">Run Python</button>
44
+ <button class="button" id="pyrite-reset" type="button">Reset example</button></div>
45
+ <p id="pyrite-status" role="status">Ready. First run downloads Python from jsDelivr.</p>
46
+ <iframe class="preview" id="pyrite-preview" title="Your Pyrite website" sandbox=""></iframe>
47
+ <noscript>This editor needs JavaScript to run Python in your browser. All other pages work without it.</noscript>
48
+ </div><script>
49
+ (() => {
50
+ const sources = ''' + payload + r''';
51
+ const editor = document.getElementById('pyrite-editor');
52
+ const initial = editor.value;
53
+ const run = document.getElementById('pyrite-run');
54
+ const status = document.getElementById('pyrite-status');
55
+ const preview = document.getElementById('pyrite-preview');
56
+ let worker, timer;
57
+ const workerSource = `
58
+ importScripts('https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js');
59
+ let runtime;
60
+ self.onmessage = async ({data}) => {
61
+ try {
62
+ if (!runtime) {
63
+ runtime = await loadPyodide();
64
+ runtime.FS.mkdirTree('/home/pyodide/pyrite');
65
+ for (const [name, source] of Object.entries(data.sources)) {
66
+ runtime.FS.writeFile('/home/pyodide/pyrite/' + name, source);
67
+ }
68
+ }
69
+ self.postMessage({ready: true});
70
+ runtime.globals.set('_editor_source', data.code);
71
+ const html = await runtime.runPythonAsync(\`
72
+ from pyrite import Page
73
+ _editor_scope = {"__name__": "__playground__"}
74
+ exec(compile(_editor_source, "playground.py", "exec"), _editor_scope)
75
+ _editor_page = _editor_scope.get("page")
76
+ if not isinstance(_editor_page, Page):
77
+ raise TypeError("Create a Page and assign it to a variable named page. Do not call app.run().")
78
+ _editor_page.render()
79
+ \`);
80
+ self.postMessage({html});
81
+ } catch (error) { self.postMessage({error: String(error)}); }
82
+ };`;
83
+ function stop() { if (worker) worker.terminate(); clearTimeout(timer); run.disabled = false; }
84
+ run.onclick = () => {
85
+ stop();
86
+ const url = URL.createObjectURL(new Blob([workerSource], {type: 'text/javascript'}));
87
+ worker = new Worker(url); URL.revokeObjectURL(url);
88
+ run.disabled = true; status.textContent = 'Loading Python…';
89
+ timer = setTimeout(() => { stop(); status.textContent = 'Python download timed out. Check your connection and try again.'; }, 120000);
90
+ worker.onmessage = ({data}) => {
91
+ if (data.ready) {
92
+ clearTimeout(timer); status.textContent = 'Running Python…';
93
+ timer = setTimeout(() => { stop(); status.textContent = 'Stopped: execution exceeded 10 seconds.'; }, 10000);
94
+ return;
95
+ }
96
+ if (data.html) { preview.srcdoc = data.html; status.textContent = 'Rendered. Edit and run again.'; }
97
+ else status.textContent = data.error;
98
+ stop();
99
+ };
100
+ worker.onerror = (event) => { status.textContent = event.message || 'Unable to load Python. Check your connection.'; stop(); };
101
+ worker.postMessage({sources, code: editor.value});
102
+ };
103
+ document.getElementById('pyrite-reset').onclick = () => { stop(); editor.value = initial; preview.srcdoc = ''; status.textContent = 'Example reset.'; };
104
+ })();
105
+ </script>'''