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.
- pyrite_web-0.1.0/LICENSE +21 -0
- pyrite_web-0.1.0/PKG-INFO +219 -0
- pyrite_web-0.1.0/README.md +209 -0
- pyrite_web-0.1.0/pyproject.toml +14 -0
- pyrite_web-0.1.0/pyrite/__init__.py +17 -0
- pyrite_web-0.1.0/pyrite/browser.py +105 -0
- pyrite_web-0.1.0/pyrite/core.py +337 -0
- pyrite_web-0.1.0/pyrite/live.py +75 -0
- pyrite_web-0.1.0/pyrite/requests.py +61 -0
- pyrite_web-0.1.0/pyrite/styles.py +112 -0
- pyrite_web-0.1.0/pyrite_web.egg-info/PKG-INFO +219 -0
- pyrite_web-0.1.0/pyrite_web.egg-info/SOURCES.txt +18 -0
- pyrite_web-0.1.0/pyrite_web.egg-info/dependency_links.txt +1 -0
- pyrite_web-0.1.0/pyrite_web.egg-info/top_level.txt +1 -0
- pyrite_web-0.1.0/setup.cfg +4 -0
- pyrite_web-0.1.0/tests/test_forms_styles.py +58 -0
- pyrite_web-0.1.0/tests/test_live_gradients.py +79 -0
- pyrite_web-0.1.0/tests/test_media_requests.py +87 -0
- pyrite_web-0.1.0/tests/test_pyrite.py +59 -0
- pyrite_web-0.1.0/tests/test_website.py +46 -0
pyrite_web-0.1.0/LICENSE
ADDED
|
@@ -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>'''
|