pyweft 0.2.1__tar.gz → 0.3.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.
- {pyweft-0.2.1 → pyweft-0.3.0}/.gitignore +1 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/CHANGELOG.md +16 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/PKG-INFO +23 -3
- {pyweft-0.2.1 → pyweft-0.3.0}/README.md +21 -1
- {pyweft-0.2.1 → pyweft-0.3.0}/docs/getting-started.md +112 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/pyproject.toml +9 -1
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/__init__.py +4 -2
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/cli.py +21 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/desktop.py +36 -7
- pyweft-0.3.0/src/pyweft/http.py +225 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/static/shell.html +6 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/static/worker.js +3 -0
- pyweft-0.3.0/src/pyweft/storage.py +164 -0
- pyweft-0.3.0/src/pyweft/webhost.py +88 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_desktop.py +45 -0
- pyweft-0.3.0/tests/test_http.py +185 -0
- pyweft-0.3.0/tests/test_storage.py +125 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/LICENSE +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/__main__.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/background.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/binding.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/component.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/dialogs.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/differ.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/dommodel.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/effects.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/events.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/expressions.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/hotreload.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/packaging.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/parser.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/prerender.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/renderer.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/routing.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/static/loading.html +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/static/runtime.js +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/store.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/styles.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/src/pyweft/vdom.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_app_settings.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_async.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_background.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_cli.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_components_kit.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_compose.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_dialogs.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_differ.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_dynamic.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_effects.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_events.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_focus.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_folders.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_hotreload.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_inline_handlers.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_invalidate.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_libraries.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_packaging.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_parser.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_prerender.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_renderer.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_routing.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_store.py +0 -0
- {pyweft-0.2.1 → pyweft-0.3.0}/tests/test_styles.py +0 -0
|
@@ -8,6 +8,22 @@ releasing moves them under the new version (see [RELEASING.md](RELEASING.md)).
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.3.0
|
|
12
|
+
|
|
13
|
+
- **`pyweft.http`**: call web APIs and load the app's data files, with the
|
|
14
|
+
same code in the browser (the page's fetch) and on the desktop (the
|
|
15
|
+
standard library). `get_json`, `get`/`post`/`put`/`patch`/`delete`,
|
|
16
|
+
`request`; JSON, form and text bodies, query parameters, timeouts. A
|
|
17
|
+
relative URL is a file the app ships (`assets/prices.json`).
|
|
18
|
+
- **`pyweft.storage`**: keep data between visits: `get`, `set`, `delete`,
|
|
19
|
+
`has`, `keys`, `clear`, for any JSON value. The browser's local storage,
|
|
20
|
+
kept apart per app; on the desktop, a JSON file in the user's data
|
|
21
|
+
folder; in tests, memory.
|
|
22
|
+
- The todo example remembers its list, and the tutorial has a step for it.
|
|
23
|
+
- The todo and shop examples are restyled with Tailwind CSS (built with
|
|
24
|
+
its CLI), in light and dark, and work at phone width. The shop has a
|
|
25
|
+
dashboard, an orders table and a reusable status badge.
|
|
26
|
+
|
|
11
27
|
## 0.2.1
|
|
12
28
|
|
|
13
29
|
- The PyPI page installs from PyPI, and its links to the guide and the
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pyweft
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A UI framework for Python: XML templates, Python code-behind, one render core for web, desktop and mobile.
|
|
5
5
|
Project-URL: Homepage, https://github.com/prassanthn/PyWeft
|
|
6
|
-
Project-URL: Documentation, https://github.
|
|
6
|
+
Project-URL: Documentation, https://prassanthn.github.io/PyWeft/
|
|
7
7
|
Project-URL: Issues, https://github.com/prassanthn/PyWeft/issues
|
|
8
8
|
Author: Prassanth Nicholas
|
|
9
9
|
License-Expression: MIT
|
|
@@ -36,6 +36,12 @@ running on your machine. No JavaScript to write, no build step.
|
|
|
36
36
|
|
|
37
37
|
*PyWeft: the weft is the thread woven through the warp to make fabric.*
|
|
38
38
|
|
|
39
|
+
**[Website](https://prassanthn.github.io/PyWeft/)**: a step-by-step
|
|
40
|
+
[tutorial](https://prassanthn.github.io/PyWeft/tutorial/), the
|
|
41
|
+
[guide](https://prassanthn.github.io/PyWeft/guide/), and the
|
|
42
|
+
[controls](https://prassanthn.github.io/PyWeft/live/gallery/) and
|
|
43
|
+
[examples](https://prassanthn.github.io/PyWeft/examples/) running live.
|
|
44
|
+
|
|
39
45
|
```xml
|
|
40
46
|
<template>
|
|
41
47
|
<div>
|
|
@@ -208,6 +214,9 @@ To see every control live, clone this repository and run
|
|
|
208
214
|
- **Files and dialogs:** `pyweft.dialogs` opens and saves files and asks for
|
|
209
215
|
confirmation with the same code on both targets: the browser's picker and
|
|
210
216
|
downloads on the web, the system's own dialogs on the desktop.
|
|
217
|
+
- **Data in and out:** `pyweft.http` calls web APIs and loads your app's
|
|
218
|
+
data files, and `pyweft.storage` keeps settings and lists between visits,
|
|
219
|
+
with the same code in the browser and on the desktop.
|
|
211
220
|
- **A fast dev loop:** hot reload that keeps your app's state, and readable
|
|
212
221
|
errors that point to your `.weft` file and line.
|
|
213
222
|
- **Fast first load:** your app's first screen is rendered ahead of time and
|
|
@@ -229,6 +238,7 @@ To see every control live, clone this repository and run
|
|
|
229
238
|
| Run it as a desktop app | [A desktop app](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#a-desktop-app-pyweft-run---desktop) |
|
|
230
239
|
| Hand out a desktop app (`.exe`, `.app`) | [Handing out a desktop app](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#handing-out-a-desktop-app-pyweft-build---desktop) |
|
|
231
240
|
| Open and save files, ask for confirmation | [Files and dialogs](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#files-and-dialogs) |
|
|
241
|
+
| Call web APIs, save data between visits | [Loading data](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#loading-data-pywefthttp), [saving data](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#saving-data-between-visits-pyweftstorage) |
|
|
232
242
|
| Name the app, give it an icon | [The app's title and icon](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#the-apps-title-and-icon) |
|
|
233
243
|
| Test components without a browser | [Using the engine from plain Python](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#6-using-the-engine-from-plain-python) |
|
|
234
244
|
|
|
@@ -282,12 +292,22 @@ With pip: `pip install -e ".[dev]"` and `pip install -e packages/pyweft-ui`.
|
|
|
282
292
|
Releases go to PyPI by pushing a version tag; [RELEASING.md](https://github.com/prassanthn/PyWeft/blob/main/RELEASING.md)
|
|
283
293
|
has the steps, and [CHANGELOG.md](https://github.com/prassanthn/PyWeft/blob/main/CHANGELOG.md) what each release changed.
|
|
284
294
|
|
|
295
|
+
The website is rebuilt and published on every push to `main`. To build it
|
|
296
|
+
locally:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
uv sync --group website
|
|
300
|
+
uv run python website/build.py
|
|
301
|
+
uv run python -m http.server -d website/_build/site # http://127.0.0.1:8000/
|
|
302
|
+
```
|
|
303
|
+
|
|
285
304
|
```
|
|
286
305
|
src/pyweft/ the framework (zero runtime dependencies)
|
|
287
306
|
static/ the browser side: runtime.js, the page shell, the worker
|
|
288
307
|
packages/pyweft-ui/ the control toolkit, released as its own package
|
|
289
|
-
docs/ the guide
|
|
308
|
+
docs/ the guide (the website splits it into pages)
|
|
290
309
|
examples/ a headless demo, the todo app and the shop (pages)
|
|
310
|
+
website/ the website: home page, tutorial, build script
|
|
291
311
|
tests/ the framework's tests
|
|
292
312
|
```
|
|
293
313
|
|
|
@@ -7,6 +7,12 @@ running on your machine. No JavaScript to write, no build step.
|
|
|
7
7
|
|
|
8
8
|
*PyWeft: the weft is the thread woven through the warp to make fabric.*
|
|
9
9
|
|
|
10
|
+
**[Website](https://prassanthn.github.io/PyWeft/)**: a step-by-step
|
|
11
|
+
[tutorial](https://prassanthn.github.io/PyWeft/tutorial/), the
|
|
12
|
+
[guide](https://prassanthn.github.io/PyWeft/guide/), and the
|
|
13
|
+
[controls](https://prassanthn.github.io/PyWeft/live/gallery/) and
|
|
14
|
+
[examples](https://prassanthn.github.io/PyWeft/examples/) running live.
|
|
15
|
+
|
|
10
16
|
```xml
|
|
11
17
|
<template>
|
|
12
18
|
<div>
|
|
@@ -179,6 +185,9 @@ To see every control live, clone this repository and run
|
|
|
179
185
|
- **Files and dialogs:** `pyweft.dialogs` opens and saves files and asks for
|
|
180
186
|
confirmation with the same code on both targets: the browser's picker and
|
|
181
187
|
downloads on the web, the system's own dialogs on the desktop.
|
|
188
|
+
- **Data in and out:** `pyweft.http` calls web APIs and loads your app's
|
|
189
|
+
data files, and `pyweft.storage` keeps settings and lists between visits,
|
|
190
|
+
with the same code in the browser and on the desktop.
|
|
182
191
|
- **A fast dev loop:** hot reload that keeps your app's state, and readable
|
|
183
192
|
errors that point to your `.weft` file and line.
|
|
184
193
|
- **Fast first load:** your app's first screen is rendered ahead of time and
|
|
@@ -200,6 +209,7 @@ To see every control live, clone this repository and run
|
|
|
200
209
|
| Run it as a desktop app | [A desktop app](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#a-desktop-app-pyweft-run---desktop) |
|
|
201
210
|
| Hand out a desktop app (`.exe`, `.app`) | [Handing out a desktop app](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#handing-out-a-desktop-app-pyweft-build---desktop) |
|
|
202
211
|
| Open and save files, ask for confirmation | [Files and dialogs](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#files-and-dialogs) |
|
|
212
|
+
| Call web APIs, save data between visits | [Loading data](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#loading-data-pywefthttp), [saving data](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#saving-data-between-visits-pyweftstorage) |
|
|
203
213
|
| Name the app, give it an icon | [The app's title and icon](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#the-apps-title-and-icon) |
|
|
204
214
|
| Test components without a browser | [Using the engine from plain Python](https://github.com/prassanthn/PyWeft/blob/main/docs/getting-started.md#6-using-the-engine-from-plain-python) |
|
|
205
215
|
|
|
@@ -253,12 +263,22 @@ With pip: `pip install -e ".[dev]"` and `pip install -e packages/pyweft-ui`.
|
|
|
253
263
|
Releases go to PyPI by pushing a version tag; [RELEASING.md](https://github.com/prassanthn/PyWeft/blob/main/RELEASING.md)
|
|
254
264
|
has the steps, and [CHANGELOG.md](https://github.com/prassanthn/PyWeft/blob/main/CHANGELOG.md) what each release changed.
|
|
255
265
|
|
|
266
|
+
The website is rebuilt and published on every push to `main`. To build it
|
|
267
|
+
locally:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
uv sync --group website
|
|
271
|
+
uv run python website/build.py
|
|
272
|
+
uv run python -m http.server -d website/_build/site # http://127.0.0.1:8000/
|
|
273
|
+
```
|
|
274
|
+
|
|
256
275
|
```
|
|
257
276
|
src/pyweft/ the framework (zero runtime dependencies)
|
|
258
277
|
static/ the browser side: runtime.js, the page shell, the worker
|
|
259
278
|
packages/pyweft-ui/ the control toolkit, released as its own package
|
|
260
|
-
docs/ the guide
|
|
279
|
+
docs/ the guide (the website splits it into pages)
|
|
261
280
|
examples/ a headless demo, the todo app and the shop (pages)
|
|
281
|
+
website/ the website: home page, tutorial, build script
|
|
262
282
|
tests/ the framework's tests
|
|
263
283
|
```
|
|
264
284
|
|
|
@@ -51,6 +51,8 @@ later mobile: only the transport differs.
|
|
|
51
51
|
[PyPI packages](#using-pypi-packages),
|
|
52
52
|
[calling JavaScript](#calling-javascript),
|
|
53
53
|
[files and dialogs](#files-and-dialogs),
|
|
54
|
+
[loading data (http)](#loading-data-pywefthttp),
|
|
55
|
+
[saving data between visits (storage)](#saving-data-between-visits-pyweftstorage),
|
|
54
56
|
[building for deployment](#building-for-deployment),
|
|
55
57
|
[a desktop app](#a-desktop-app-pyweft-run---desktop),
|
|
56
58
|
[handing it out](#handing-out-a-desktop-app-pyweft-build---desktop)
|
|
@@ -1694,6 +1696,21 @@ page:
|
|
|
1694
1696
|
`@apply` works in `_tailwind/input.css`, but not in a component's `<style>`
|
|
1695
1697
|
block, which Tailwind never reads.
|
|
1696
1698
|
|
|
1699
|
+
**Classes that depend on state.** A `:class` sets the whole attribute, so
|
|
1700
|
+
an element with both `class` and `:class` gets only the `:class` one. Put
|
|
1701
|
+
the fixed classes in the expression too:
|
|
1702
|
+
|
|
1703
|
+
```xml
|
|
1704
|
+
<button @click="show = name"
|
|
1705
|
+
:class="'rounded-lg px-3 py-1.5 ' + ('bg-slate-900 text-white' if name == show else 'text-slate-500')">
|
|
1706
|
+
{{ name }}
|
|
1707
|
+
</button>
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
The todo and shop examples (`examples/todo`, `examples/shop`) are styled
|
|
1711
|
+
with Tailwind's CLI exactly as above: see each one's `_tailwind/input.css`
|
|
1712
|
+
for the setup and the command that rebuilds its `tailwind.css`.
|
|
1713
|
+
|
|
1697
1714
|
### Ready-made controls: pyweft-ui
|
|
1698
1715
|
|
|
1699
1716
|
If you'd rather not write HTML and CSS at all, `pyweft-ui` gives you styled,
|
|
@@ -1869,6 +1886,101 @@ async def clear(self):
|
|
|
1869
1886
|
- **In tests**, `dialogs.set_host()` installs a stand-in that answers
|
|
1870
1887
|
instead of the user; without one, the calls raise an error saying so.
|
|
1871
1888
|
|
|
1889
|
+
### Loading data: pyweft.http
|
|
1890
|
+
|
|
1891
|
+
`pyweft.http` calls web APIs and loads files, the same way in the browser
|
|
1892
|
+
and in a [desktop app](#a-desktop-app-pyweft-run---desktop). Requests are
|
|
1893
|
+
`async`, so make them in `async` handlers or an `async def on_init`:
|
|
1894
|
+
|
|
1895
|
+
```python
|
|
1896
|
+
from pyweft import http
|
|
1897
|
+
|
|
1898
|
+
orders: list = []
|
|
1899
|
+
error: str = ""
|
|
1900
|
+
|
|
1901
|
+
async def on_init(self):
|
|
1902
|
+
self.orders = await http.get_json("https://api.example.com/orders")
|
|
1903
|
+
|
|
1904
|
+
async def save(self, order):
|
|
1905
|
+
response = await http.post("https://api.example.com/orders", json=order)
|
|
1906
|
+
if not response.ok:
|
|
1907
|
+
self.error = f"Couldn't save the order ({response.status})"
|
|
1908
|
+
```
|
|
1909
|
+
|
|
1910
|
+
| Call | Returns |
|
|
1911
|
+
|---|---|
|
|
1912
|
+
| `get_json(url)` | the body parsed as JSON; `http.HTTPError` unless the status is 2xx |
|
|
1913
|
+
| `get(url)`, `post(url)`, `put(url)`, `patch(url)`, `delete(url)` | a `Response`, whatever its status |
|
|
1914
|
+
| `request(method, url)` | the same, for any method |
|
|
1915
|
+
|
|
1916
|
+
Each takes `params=` (added to the URL's query), `headers=`, `timeout=`
|
|
1917
|
+
(seconds, 30 by default) and a body: `json=` (any JSON value) or `data=`
|
|
1918
|
+
(bytes, text, or a dict sent as a form).
|
|
1919
|
+
|
|
1920
|
+
- **A `Response`** has `status`, `ok` (a 2xx status), `headers` (names in
|
|
1921
|
+
lower case), `data` (the bytes), `text` and `json()`.
|
|
1922
|
+
`raise_for_status()` raises `http.HTTPError` unless it's 2xx.
|
|
1923
|
+
- **A relative URL is a file your app ships.** Put data files in
|
|
1924
|
+
`assets/`, and `await http.get_json("assets/prices.json")` reads them in
|
|
1925
|
+
the browser and in a desktop window alike.
|
|
1926
|
+
- **Failures:** no answer at all (offline, a mistyped address) raises
|
|
1927
|
+
`http.NetworkError`; a slow server raises `TimeoutError`.
|
|
1928
|
+
- **In the browser, the browser's rules apply.** A request to another site
|
|
1929
|
+
only works if that server allows it (it must send CORS headers; public
|
|
1930
|
+
APIs usually do). There is no such rule in a desktop app.
|
|
1931
|
+
- **Everyone can see the requests a web app makes**, as with any web page,
|
|
1932
|
+
so an API key in your code isn't secret. Keep secrets on a server of
|
|
1933
|
+
your own and call that.
|
|
1934
|
+
- **In tests**, requests go out for real through the standard library.
|
|
1935
|
+
`http.set_host()` installs a stand-in instead: an object with `async
|
|
1936
|
+
request(method, url, headers, body, timeout)` returning `(status,
|
|
1937
|
+
headers, data, url)`.
|
|
1938
|
+
|
|
1939
|
+
### Saving data between visits: pyweft.storage
|
|
1940
|
+
|
|
1941
|
+
`pyweft.storage` keeps small amounts of data, such as settings, a draft or
|
|
1942
|
+
a list, until the next visit:
|
|
1943
|
+
|
|
1944
|
+
```python
|
|
1945
|
+
from pyweft import effect, storage
|
|
1946
|
+
|
|
1947
|
+
items: list = []
|
|
1948
|
+
|
|
1949
|
+
def on_init(self):
|
|
1950
|
+
self.items = storage.get("todos", []) # [] the first time
|
|
1951
|
+
|
|
1952
|
+
@effect
|
|
1953
|
+
def keep(self):
|
|
1954
|
+
storage.set("todos", self.items) # runs again whenever items changes
|
|
1955
|
+
```
|
|
1956
|
+
|
|
1957
|
+
| Call | Does |
|
|
1958
|
+
|---|---|
|
|
1959
|
+
| `get(key, default=None)` | the stored value, or `default` |
|
|
1960
|
+
| `set(key, value)` | stores `value`, replacing what was there |
|
|
1961
|
+
| `delete(key)`, `clear()` | removes one key, or everything this app stored |
|
|
1962
|
+
| `has(key)`, `keys()` | whether a key has a value; every key, sorted |
|
|
1963
|
+
|
|
1964
|
+
- **Values are anything JSON can hold:** numbers, text, `True`/`False`/
|
|
1965
|
+
`None`, and lists and dicts of those. Anything else raises `TypeError`;
|
|
1966
|
+
turn dates into text first, for example.
|
|
1967
|
+
- **`get` returns a copy.** Changing a list you got back doesn't change
|
|
1968
|
+
what's stored until you `set` it again, which is what the effect above
|
|
1969
|
+
does.
|
|
1970
|
+
- **The calls aren't `async`**, so they work anywhere: in `on_init`, an
|
|
1971
|
+
effect, a property, or a module of your app.
|
|
1972
|
+
- **Where it lives:** in the browser, the browser's local storage for your
|
|
1973
|
+
site (about 5 MB in all, cleared if the visitor clears site data), kept
|
|
1974
|
+
apart per app by the `[project]` name in its `pyproject.toml`. In a
|
|
1975
|
+
desktop app, a JSON file in the user's data folder:
|
|
1976
|
+
`%APPDATA%\<name>\storage.json` on Windows,
|
|
1977
|
+
`~/Library/Application Support/<name>/` on macOS,
|
|
1978
|
+
`~/.local/share/<name>/` on Linux.
|
|
1979
|
+
- **Not in `run_in_background`:** the worker can't reach the page's
|
|
1980
|
+
storage, so read what the function needs first and pass it in.
|
|
1981
|
+
- **In tests** it lives in memory. `storage.set_host(storage.MemoryStorage())`
|
|
1982
|
+
starts a test empty; `storage.FileStorage(path)` keeps it in a file.
|
|
1983
|
+
|
|
1872
1984
|
### Building for deployment
|
|
1873
1985
|
|
|
1874
1986
|
```bash
|
|
@@ -35,7 +35,7 @@ package = ["pywebview>=5", "pyinstaller>=6"]
|
|
|
35
35
|
|
|
36
36
|
[project.urls]
|
|
37
37
|
Homepage = "https://github.com/prassanthn/PyWeft"
|
|
38
|
-
Documentation = "https://github.
|
|
38
|
+
Documentation = "https://prassanthn.github.io/PyWeft/"
|
|
39
39
|
Issues = "https://github.com/prassanthn/PyWeft/issues"
|
|
40
40
|
|
|
41
41
|
[project.scripts]
|
|
@@ -70,3 +70,11 @@ pyweft-ui = { workspace = true }
|
|
|
70
70
|
|
|
71
71
|
[dependency-groups]
|
|
72
72
|
dev = ["pyweft-ui"]
|
|
73
|
+
# The website (website/build.py): uv sync --group website
|
|
74
|
+
# and the examples' Tailwind stylesheets (examples/*/_tailwind/input.css)
|
|
75
|
+
website = [
|
|
76
|
+
"mkdocs-material>=9.8,<10; python_version >= '3.11'",
|
|
77
|
+
"pytailwindcss>=0.4; python_version >= '3.11'",
|
|
78
|
+
# the todo example's own dependency, so its first screen can be pre-rendered
|
|
79
|
+
"humanize",
|
|
80
|
+
]
|
|
@@ -4,7 +4,7 @@ Markup in XML, logic in Python, one render core that emits transport-agnostic
|
|
|
4
4
|
patches so desktop (webview), web (Pyodide) and mobile can share a codebase.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from . import dialogs
|
|
7
|
+
from . import dialogs, http, storage
|
|
8
8
|
from .background import BackgroundError, run_in_background
|
|
9
9
|
from .component import (
|
|
10
10
|
Component,
|
|
@@ -24,7 +24,7 @@ from .store import Store
|
|
|
24
24
|
from .styles import collect_styles, scope_css
|
|
25
25
|
from .vdom import Element, Node, Text, to_dicts
|
|
26
26
|
|
|
27
|
-
__version__ = "0.
|
|
27
|
+
__version__ = "0.3.0"
|
|
28
28
|
|
|
29
29
|
__all__ = [
|
|
30
30
|
"Component",
|
|
@@ -43,6 +43,8 @@ __all__ = [
|
|
|
43
43
|
"Store",
|
|
44
44
|
"set_title",
|
|
45
45
|
"dialogs",
|
|
46
|
+
"http",
|
|
47
|
+
"storage",
|
|
46
48
|
"collect_styles",
|
|
47
49
|
"scope_css",
|
|
48
50
|
"diff",
|
|
@@ -234,6 +234,26 @@ def app_identity(app_dir: Path) -> AppIdentity:
|
|
|
234
234
|
return AppIdentity(title=title.strip(), icon=icon)
|
|
235
235
|
|
|
236
236
|
|
|
237
|
+
def app_id(app_dir: Path) -> str:
|
|
238
|
+
"""A stable name for keeping the app's data apart (pyweft.storage): the
|
|
239
|
+
``[project]`` name from its pyproject.toml, else the folder's name.
|
|
240
|
+
Renaming the project starts its saved data afresh."""
|
|
241
|
+
name = ""
|
|
242
|
+
pyproject = app_dir / "pyproject.toml"
|
|
243
|
+
if pyproject.exists():
|
|
244
|
+
text = pyproject.read_text(encoding="utf-8")
|
|
245
|
+
if tomllib is not None:
|
|
246
|
+
try:
|
|
247
|
+
name = tomllib.loads(text).get("project", {}).get("name", "")
|
|
248
|
+
except tomllib.TOMLDecodeError as exc:
|
|
249
|
+
raise CliError(f"{pyproject}: {exc}") from None
|
|
250
|
+
else:
|
|
251
|
+
name = _flat_table(text, "project").get("name", "")
|
|
252
|
+
if not isinstance(name, str) or not name.strip():
|
|
253
|
+
name = app_dir.resolve().name
|
|
254
|
+
return re.sub(r"[^a-z0-9._-]+", "-", name.strip().lower()).strip("-.") or "app"
|
|
255
|
+
|
|
256
|
+
|
|
237
257
|
# mimetypes reads the Windows registry, which can be wrong or missing these.
|
|
238
258
|
_CONTENT_TYPES = {
|
|
239
259
|
".css": "text/css",
|
|
@@ -515,6 +535,7 @@ def render_shell(
|
|
|
515
535
|
"__PYWEFT_LIBRARIES__",
|
|
516
536
|
json.dumps([{"name": lib.name, "files": lib.files} for lib in app_libraries(app_dir)]),
|
|
517
537
|
)
|
|
538
|
+
.replace("__PYWEFT_APP_ID__", json.dumps(app_id(app_dir)))
|
|
518
539
|
.replace("__PYWEFT_DEV__", "true" if dev else "false")
|
|
519
540
|
.replace("__PYWEFT_SINCE__", str(since))
|
|
520
541
|
.replace("__PYWEFT_CSS__", css)
|
|
@@ -22,6 +22,7 @@ from __future__ import annotations
|
|
|
22
22
|
import asyncio
|
|
23
23
|
import concurrent.futures
|
|
24
24
|
import json
|
|
25
|
+
import os
|
|
25
26
|
import re
|
|
26
27
|
import sys
|
|
27
28
|
import threading
|
|
@@ -32,7 +33,7 @@ from pathlib import Path
|
|
|
32
33
|
from typing import Any, Callable
|
|
33
34
|
from urllib.parse import unquote
|
|
34
35
|
|
|
35
|
-
from . import dialogs, routing
|
|
36
|
+
from . import dialogs, http, routing, storage
|
|
36
37
|
from .component import load_component
|
|
37
38
|
from .differ import diff
|
|
38
39
|
from .hotreload import format_error, hot_swap
|
|
@@ -225,9 +226,20 @@ class DesktopApp:
|
|
|
225
226
|
"""One app, one window. ``window`` is anything with ``evaluate_js``:
|
|
226
227
|
a pywebview window, or a stand-in in tests."""
|
|
227
228
|
|
|
228
|
-
def __init__(
|
|
229
|
+
def __init__(
|
|
230
|
+
self,
|
|
231
|
+
app_dir: Path,
|
|
232
|
+
window: Any = None,
|
|
233
|
+
base_url: str | None = None,
|
|
234
|
+
storage_file: Path | None = None,
|
|
235
|
+
):
|
|
229
236
|
self.app_dir = Path(app_dir).resolve()
|
|
230
237
|
self.window = window
|
|
238
|
+
# Where the window's page is served, so relative pyweft.http URLs
|
|
239
|
+
# (assets/data.json) work as they do in the browser.
|
|
240
|
+
self.base_url = base_url
|
|
241
|
+
# pyweft.storage's file; in memory without one (tests).
|
|
242
|
+
self.storage_file = storage_file
|
|
231
243
|
self.here = "/"
|
|
232
244
|
self.tree: list = []
|
|
233
245
|
self.renderer: Renderer | None = None
|
|
@@ -281,6 +293,10 @@ class DesktopApp:
|
|
|
281
293
|
sys.path.insert(0, str(self.app_dir)) # the app's own modules
|
|
282
294
|
routing.set_host(_Location(self))
|
|
283
295
|
dialogs.set_host(_Dialogs(self))
|
|
296
|
+
http.set_host(http.Urllib(base_url=self.base_url))
|
|
297
|
+
storage.set_host(
|
|
298
|
+
storage.FileStorage(self.storage_file) if self.storage_file else storage.MemoryStorage()
|
|
299
|
+
)
|
|
284
300
|
self.cls = load_component(self.app_dir / "app.weft")
|
|
285
301
|
self.renderer = Renderer(self.cls())
|
|
286
302
|
# Already on the loop thread whenever app code changes state, except
|
|
@@ -539,10 +555,22 @@ def watch(app: DesktopApp, stop: threading.Event, interval: float = 0.3) -> None
|
|
|
539
555
|
app.js("location.reload()") # head.html, assets: the page remounts
|
|
540
556
|
|
|
541
557
|
|
|
558
|
+
def data_dir(app_id: str) -> Path:
|
|
559
|
+
"""Where a desktop app keeps its data (pyweft.storage): the user's
|
|
560
|
+
application data folder, in a folder named after the app."""
|
|
561
|
+
if sys.platform == "win32":
|
|
562
|
+
base = os.environ.get("APPDATA") or Path.home() / "AppData" / "Roaming"
|
|
563
|
+
elif sys.platform == "darwin":
|
|
564
|
+
base = Path.home() / "Library" / "Application Support"
|
|
565
|
+
else:
|
|
566
|
+
base = os.environ.get("XDG_DATA_HOME") or Path.home() / ".local" / "share"
|
|
567
|
+
return Path(base) / app_id
|
|
568
|
+
|
|
569
|
+
|
|
542
570
|
def run_desktop(app_dir: Path, dev: bool = True) -> None:
|
|
543
571
|
"""The app in a window until it closes. ``dev`` (``pyweft run``) adds
|
|
544
572
|
hot reload and terminal messages; a packaged app runs without them."""
|
|
545
|
-
from .cli import CliError, app_identity, app_pyweft_files
|
|
573
|
+
from .cli import CliError, app_id, app_identity, app_pyweft_files
|
|
546
574
|
|
|
547
575
|
app_pyweft_files(app_dir) # fails fast without an app.weft
|
|
548
576
|
identity = app_identity(app_dir) # and on a mistake in the settings
|
|
@@ -555,15 +583,16 @@ def run_desktop(app_dir: Path, dev: bool = True) -> None:
|
|
|
555
583
|
'uv add "pyweft[desktop]" (or: pip install "pyweft[desktop]")'
|
|
556
584
|
) from None
|
|
557
585
|
|
|
558
|
-
|
|
586
|
+
server = make_server(app_dir)
|
|
587
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
588
|
+
url = f"http://127.0.0.1:{server.server_address[1]}/"
|
|
589
|
+
app = DesktopApp(app_dir, base_url=url, storage_file=data_dir(app_id(app_dir)) / "storage.json")
|
|
559
590
|
try:
|
|
560
591
|
app.start()
|
|
561
592
|
except Exception as exc: # noqa: BLE001 - shown as a CLI error
|
|
562
593
|
app.stop()
|
|
594
|
+
server.shutdown()
|
|
563
595
|
raise CliError(format_error(exc, root=str(Path(app_dir).resolve()))) from None
|
|
564
|
-
server = make_server(app_dir)
|
|
565
|
-
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
566
|
-
url = f"http://127.0.0.1:{server.server_address[1]}/"
|
|
567
596
|
app.window = webview.create_window(identity.title, url, js_api=app.api, **options)
|
|
568
597
|
stop = threading.Event()
|
|
569
598
|
if dev:
|