pyjs-examples 0.0.2__tar.gz → 0.4.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.
- pyjs_examples-0.4.0/LICENSE +28 -0
- pyjs_examples-0.4.0/PKG-INFO +108 -0
- pyjs_examples-0.4.0/README.md +73 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/README.md +21 -5
- pyjs_examples-0.4.0/calculator/__init__.py +1 -0
- pyjs_examples-0.4.0/calculator/__main__.py +21 -0
- pyjs_examples-0.4.0/calculator/android.py +225 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/calculator.py +15 -17
- pyjs_examples-0.4.0/calculator/tests/test_android_calculator.py +34 -0
- pyjs_examples-0.4.0/calculator/tests/test_android_calculator_model.py +26 -0
- pyjs_examples-0.4.0/chat/README.md +76 -0
- {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/__main__.py +3 -3
- pyjs_examples-0.4.0/chat/chat.py +1165 -0
- {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/chat_server.py +79 -36
- pyjs_examples-0.4.0/chat/protocol.py +28 -0
- pyjs_examples-0.4.0/chat/tests/test_browser_chat.py +149 -0
- {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/tests/test_chat_server.py +50 -2
- {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/visual_test.py +68 -13
- pyjs_examples-0.4.0/components/README.md +38 -0
- pyjs_examples-0.4.0/components/__init__.py +1 -0
- pyjs_examples-0.4.0/components/__main__.py +23 -0
- pyjs_examples-0.4.0/components/app.py +95 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0/components}/catalog.py +359 -56
- pyjs_examples-0.4.0/components/catalog_sources.py +301 -0
- pyjs_examples-0.4.0/components/tests/test_browser_navigation.py +58 -0
- {pyjs_examples-0.0.2/workspace → pyjs_examples-0.4.0/components}/tests/test_browser_workspace.py +1 -1
- pyjs_examples-0.4.0/components/tests/test_components.py +68 -0
- {pyjs_examples-0.0.2/workspace → pyjs_examples-0.4.0/components}/tests/test_workspace.py +3 -1
- {pyjs_examples-0.0.2/workspace → pyjs_examples-0.4.0/components}/workspace.py +10 -14
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/counter.py +1 -1
- pyjs_examples-0.4.0/notes/README.md +48 -0
- pyjs_examples-0.4.0/notes/__init__.py +5 -0
- pyjs_examples-0.4.0/notes/__main__.py +18 -0
- pyjs_examples-0.4.0/notes/mobile_assets/fonts/LICENSE +202 -0
- pyjs_examples-0.4.0/notes/mobile_assets/fonts/MaterialIcons-Regular.ttf +0 -0
- pyjs_examples-0.4.0/notes/notes.py +1016 -0
- pyjs_examples-0.4.0/notes/tests/renders/android/notes-actions.png +0 -0
- pyjs_examples-0.4.0/notes/tests/renders/android/notes-checklist-editor.png +0 -0
- pyjs_examples-0.4.0/notes/tests/renders/android/notes-home.png +0 -0
- pyjs_examples-0.4.0/notes/tests/renders/android/notes-list.png +0 -0
- pyjs_examples-0.4.0/notes/tests/renders/android/notes-navigation.png +0 -0
- pyjs_examples-0.4.0/notes/tests/test_android_notes.py +116 -0
- pyjs_examples-0.4.0/notes/tests/test_browser_web_notes.py +149 -0
- pyjs_examples-0.4.0/notes/tests/test_notes.py +181 -0
- pyjs_examples-0.4.0/notes/tests/test_web_notes.py +56 -0
- {pyjs_examples-0.0.2/kanban → pyjs_examples-0.4.0/notes}/visual_test.py +29 -25
- pyjs_examples-0.0.2/kanban/kanban.py → pyjs_examples-0.4.0/notes/web.py +159 -89
- pyjs_examples-0.4.0/pyjs_examples.egg-info/PKG-INFO +108 -0
- pyjs_examples-0.4.0/pyjs_examples.egg-info/SOURCES.txt +157 -0
- pyjs_examples-0.4.0/pyjs_examples.egg-info/entry_points.txt +7 -0
- pyjs_examples-0.4.0/pyjs_examples.egg-info/requires.txt +14 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/pyproject.toml +44 -37
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/README.md +8 -7
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/__main__.py +1 -1
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/space_invaders.py +69 -18
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/tests/test_browser_game.py +27 -2
- pyjs_examples-0.4.0/store/README.md +134 -0
- pyjs_examples-0.4.0/store/__init__.py +1 -0
- pyjs_examples-0.4.0/store/__main__.py +67 -0
- pyjs_examples-0.4.0/store/api.py +584 -0
- pyjs_examples-0.4.0/store/apps.py +7 -0
- pyjs_examples-0.4.0/store/core.py +137 -0
- pyjs_examples-0.4.0/store/fixtures/store.json +258 -0
- pyjs_examples-0.4.0/store/models.py +93 -0
- pyjs_examples-0.4.0/store/server.py +192 -0
- pyjs_examples-0.4.0/store/settings.py +23 -0
- pyjs_examples-0.4.0/store/store.py +1776 -0
- pyjs_examples-0.4.0/store/tests/conftest.py +33 -0
- pyjs_examples-0.4.0/store/tests/test_browser_store.py +533 -0
- pyjs_examples-0.4.0/store/tests/test_store.py +987 -0
- pyjs_examples-0.4.0/store/urls.py +28 -0
- pyjs_examples-0.4.0/store/visual_test.py +129 -0
- pyjs_examples-0.0.2/PKG-INFO +0 -72
- pyjs_examples-0.0.2/README.md +0 -48
- pyjs_examples-0.0.2/calculator/__init__.py +0 -1
- pyjs_examples-0.0.2/calculator/__main__.py +0 -15
- pyjs_examples-0.0.2/chatroom/README.md +0 -50
- pyjs_examples-0.0.2/chatroom/chatroom.py +0 -707
- pyjs_examples-0.0.2/chatroom/tests/test_browser_chatroom.py +0 -63
- pyjs_examples-0.0.2/kanban/README.md +0 -31
- pyjs_examples-0.0.2/kanban/__init__.py +0 -1
- pyjs_examples-0.0.2/kanban/__main__.py +0 -8
- pyjs_examples-0.0.2/kanban/tests/test_browser_kanban.py +0 -155
- pyjs_examples-0.0.2/kanban/tests/test_kanban.py +0 -38
- pyjs_examples-0.0.2/nativescript_counter/README.md +0 -31
- pyjs_examples-0.0.2/nativescript_counter/__init__.py +0 -5
- pyjs_examples-0.0.2/nativescript_counter/__main__.py +0 -17
- pyjs_examples-0.0.2/nativescript_counter/counter.py +0 -54
- pyjs_examples-0.0.2/nativescript_counter/tests/test_android_counter.py +0 -31
- pyjs_examples-0.0.2/nativescript_counter/tests/test_counter.py +0 -11
- pyjs_examples-0.0.2/navigation/README.md +0 -20
- pyjs_examples-0.0.2/navigation/__init__.py +0 -1
- pyjs_examples-0.0.2/navigation/__main__.py +0 -49
- pyjs_examples-0.0.2/navigation/navigation.py +0 -800
- pyjs_examples-0.0.2/navigation/tests/test_browser_navigation.py +0 -83
- pyjs_examples-0.0.2/navigation/tests/test_navigation.py +0 -59
- pyjs_examples-0.0.2/pyjs_examples.egg-info/PKG-INFO +0 -72
- pyjs_examples-0.0.2/pyjs_examples.egg-info/SOURCES.txt +0 -154
- pyjs_examples-0.0.2/pyjs_examples.egg-info/entry_points.txt +0 -12
- pyjs_examples-0.0.2/pyjs_examples.egg-info/requires.txt +0 -4
- pyjs_examples-0.0.2/samples/__init__.py +0 -1
- pyjs_examples-0.0.2/samples/state_counter/README.md +0 -19
- pyjs_examples-0.0.2/samples/state_counter/__init__.py +0 -1
- pyjs_examples-0.0.2/samples/state_counter/__main__.py +0 -3
- pyjs_examples-0.0.2/samples/state_counter/state_counter.py +0 -60
- pyjs_examples-0.0.2/samples/state_counter/tests/test_browser_state_counter.py +0 -18
- pyjs_examples-0.0.2/samples/state_counter/tests/test_state_counter.py +0 -17
- pyjs_examples-0.0.2/samples/ui_dialog/README.md +0 -19
- pyjs_examples-0.0.2/samples/ui_dialog/__init__.py +0 -1
- pyjs_examples-0.0.2/samples/ui_dialog/__main__.py +0 -3
- pyjs_examples-0.0.2/samples/ui_dialog/tests/test_browser_ui_dialog.py +0 -21
- pyjs_examples-0.0.2/samples/ui_dialog/tests/test_ui_dialog.py +0 -10
- pyjs_examples-0.0.2/samples/ui_dialog/ui_dialog.py +0 -65
- pyjs_examples-0.0.2/todomvc/README.md +0 -50
- pyjs_examples-0.0.2/todomvc/__init__.py +0 -1
- pyjs_examples-0.0.2/todomvc/__main__.py +0 -12
- pyjs_examples-0.0.2/todomvc/base.css +0 -141
- pyjs_examples-0.0.2/todomvc/index.css +0 -393
- pyjs_examples-0.0.2/todomvc/tests/test_browser_todomvc.py +0 -152
- pyjs_examples-0.0.2/todomvc/tests/test_todomvc.py +0 -84
- pyjs_examples-0.0.2/todomvc/todomvc.py +0 -428
- pyjs_examples-0.0.2/workspace/README.md +0 -25
- pyjs_examples-0.0.2/workspace/__init__.py +0 -1
- pyjs_examples-0.0.2/workspace/__main__.py +0 -3
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/tests/test_browser_calculator.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/tests/test_calculator.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/calculator/tests/test_pwa.py +0 -0
- {pyjs_examples-0.0.2/chatroom → pyjs_examples-0.4.0/chat}/__init__.py +0 -0
- /pyjs_examples-0.0.2/kanban/tests/renders/kanban-board.png → /pyjs_examples-0.4.0/notes/tests/renders/notes-board.png +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/pyjs_examples.egg-info/dependency_links.txt +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/pyjs_examples.egg-info/top_level.txt +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/setup.cfg +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/tests/renders/action_scene.png +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/tests/renders/initial_wave.png +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.4.0}/space_invaders/tests/test_game.py +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Lex Berezhny
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyjs-examples
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Official example applications for PyJS.
|
|
5
|
+
Author-email: Lex Berezhny <lex@damoti.com>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/pyjs/pyjs
|
|
8
|
+
Project-URL: Source, https://github.com/pyjs/pyjs
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.14
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pyjs==0.4.0
|
|
23
|
+
Provides-Extra: store
|
|
24
|
+
Requires-Dist: django<6.2,>=6.0; extra == "store"
|
|
25
|
+
Requires-Dist: pydantic<3,>=2; extra == "store"
|
|
26
|
+
Requires-Dist: pyjs-extras==0.4.0; extra == "store"
|
|
27
|
+
Requires-Dist: uvicorn<1,>=0.52; extra == "store"
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: django<6.2,>=6.0; extra == "test"
|
|
30
|
+
Requires-Dist: pydantic<3,>=2; extra == "test"
|
|
31
|
+
Requires-Dist: pyjs-extras==0.4.0; extra == "test"
|
|
32
|
+
Requires-Dist: pytest>=9.0; extra == "test"
|
|
33
|
+
Requires-Dist: uvicorn<1,>=0.52; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# PyJS Examples
|
|
37
|
+
|
|
38
|
+
This directory is the source tree for the separately installable
|
|
39
|
+
`pyjs-examples` distribution. Setuptools maps it to the `pyjs_examples` import
|
|
40
|
+
package while keeping each application easy to browse in the repository.
|
|
41
|
+
|
|
42
|
+
Install the core and examples from a source checkout:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv sync
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
An installed release provides the examples through an extra:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python -m pip install "pyjs[examples]"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Application responsibilities
|
|
55
|
+
|
|
56
|
+
The examples progress from small programs to composed, full-stack systems.
|
|
57
|
+
Features may overlap when that makes an application more realistic.
|
|
58
|
+
|
|
59
|
+
| Example | Role | Principal PyJS coverage |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| [Counter](counter.py) | Smallest app | DOM, events, one-file tests |
|
|
62
|
+
| [Calculator](calculator/) | Universal app | Web, PWA, Android, SSR, state |
|
|
63
|
+
| [Components](components/) | UI reference | Widgets, forms, navigation |
|
|
64
|
+
| [Notes](notes/) | Personal app | Web, Android, drag/drop, restore |
|
|
65
|
+
| [Chat](chat/) | Real-time starter | WebSockets, multi-window, plans |
|
|
66
|
+
| [Store](store/) | Business app | Django, JSON:API, SSE, themes, cursor feeds |
|
|
67
|
+
| [Space Invaders](space_invaders/) | GPU game | WebGPU, input, render tests |
|
|
68
|
+
|
|
69
|
+
Run an application with its module name, for example:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python -m pyjs_examples.calculator
|
|
73
|
+
python -m pyjs_examples.components
|
|
74
|
+
python -m pyjs_examples.notes
|
|
75
|
+
python -m pyjs_examples.chat
|
|
76
|
+
python -m pyjs_examples.store
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Install `pyjs-examples[store]` before running Store from a release package.
|
|
80
|
+
|
|
81
|
+
Calculator and Notes accept `--android` to build and launch their NativeScript
|
|
82
|
+
versions. Their default command serves the web version.
|
|
83
|
+
|
|
84
|
+
Components is also the source and test surface for the planned website
|
|
85
|
+
component reference. It combines the former catalog, workspace, navigation,
|
|
86
|
+
and focused dialog sample in one application.
|
|
87
|
+
|
|
88
|
+
Notes combines the former Kanban board with the mobile notes application. It
|
|
89
|
+
uses DOM-owned ordering, drag/drop, server restoration, and visual tests.
|
|
90
|
+
|
|
91
|
+
Store is the canonical Django and typed client/server example. It also owns the
|
|
92
|
+
themability story, signed cursor feeds, infinite scrolling, and data-driven
|
|
93
|
+
charts.
|
|
94
|
+
|
|
95
|
+
## Templates
|
|
96
|
+
|
|
97
|
+
List, inspect, or copy examples using the core scaffolding command:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pyjs template
|
|
101
|
+
pyjs template pyjs_examples.calculator
|
|
102
|
+
pyjs create my-calculator --template pyjs_examples.calculator
|
|
103
|
+
pyjs create --single-file my_counter.py --template pyjs_examples.counter
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The distribution registers modules and packages through the `pyjs.templates`
|
|
107
|
+
entry-point group. PyJS infers file and directory templates from normal Python
|
|
108
|
+
module structure instead of hardcoding a catalog.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# PyJS Examples
|
|
2
|
+
|
|
3
|
+
This directory is the source tree for the separately installable
|
|
4
|
+
`pyjs-examples` distribution. Setuptools maps it to the `pyjs_examples` import
|
|
5
|
+
package while keeping each application easy to browse in the repository.
|
|
6
|
+
|
|
7
|
+
Install the core and examples from a source checkout:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv sync
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
An installed release provides the examples through an extra:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m pip install "pyjs[examples]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Application responsibilities
|
|
20
|
+
|
|
21
|
+
The examples progress from small programs to composed, full-stack systems.
|
|
22
|
+
Features may overlap when that makes an application more realistic.
|
|
23
|
+
|
|
24
|
+
| Example | Role | Principal PyJS coverage |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| [Counter](counter.py) | Smallest app | DOM, events, one-file tests |
|
|
27
|
+
| [Calculator](calculator/) | Universal app | Web, PWA, Android, SSR, state |
|
|
28
|
+
| [Components](components/) | UI reference | Widgets, forms, navigation |
|
|
29
|
+
| [Notes](notes/) | Personal app | Web, Android, drag/drop, restore |
|
|
30
|
+
| [Chat](chat/) | Real-time starter | WebSockets, multi-window, plans |
|
|
31
|
+
| [Store](store/) | Business app | Django, JSON:API, SSE, themes, cursor feeds |
|
|
32
|
+
| [Space Invaders](space_invaders/) | GPU game | WebGPU, input, render tests |
|
|
33
|
+
|
|
34
|
+
Run an application with its module name, for example:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m pyjs_examples.calculator
|
|
38
|
+
python -m pyjs_examples.components
|
|
39
|
+
python -m pyjs_examples.notes
|
|
40
|
+
python -m pyjs_examples.chat
|
|
41
|
+
python -m pyjs_examples.store
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Install `pyjs-examples[store]` before running Store from a release package.
|
|
45
|
+
|
|
46
|
+
Calculator and Notes accept `--android` to build and launch their NativeScript
|
|
47
|
+
versions. Their default command serves the web version.
|
|
48
|
+
|
|
49
|
+
Components is also the source and test surface for the planned website
|
|
50
|
+
component reference. It combines the former catalog, workspace, navigation,
|
|
51
|
+
and focused dialog sample in one application.
|
|
52
|
+
|
|
53
|
+
Notes combines the former Kanban board with the mobile notes application. It
|
|
54
|
+
uses DOM-owned ordering, drag/drop, server restoration, and visual tests.
|
|
55
|
+
|
|
56
|
+
Store is the canonical Django and typed client/server example. It also owns the
|
|
57
|
+
themability story, signed cursor feeds, infinite scrolling, and data-driven
|
|
58
|
+
charts.
|
|
59
|
+
|
|
60
|
+
## Templates
|
|
61
|
+
|
|
62
|
+
List, inspect, or copy examples using the core scaffolding command:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pyjs template
|
|
66
|
+
pyjs template pyjs_examples.calculator
|
|
67
|
+
pyjs create my-calculator --template pyjs_examples.calculator
|
|
68
|
+
pyjs create --single-file my_counter.py --template pyjs_examples.counter
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The distribution registers modules and packages through the `pyjs.templates`
|
|
72
|
+
entry-point group. PyJS infers file and directory templates from normal Python
|
|
73
|
+
module structure instead of hardcoding a catalog.
|
|
@@ -6,7 +6,8 @@ example for pyjs browser-target testing.
|
|
|
6
6
|
It demonstrates:
|
|
7
7
|
|
|
8
8
|
- calculator state and behavior owned by a custom element
|
|
9
|
-
- typed `state()` fields serialized to DOM attributes and rendered with
|
|
9
|
+
- typed `state()` fields serialized to DOM attributes and rendered with
|
|
10
|
+
`@effect`
|
|
10
11
|
- a centered responsive custom-element UI
|
|
11
12
|
- digit and decimal entry
|
|
12
13
|
- addition, subtraction, multiplication, and division
|
|
@@ -14,6 +15,7 @@ It demonstrates:
|
|
|
14
15
|
- asynchronous result persistence through an HTTP POST route
|
|
15
16
|
- per-browser in-memory sessions that restore the last result after refresh
|
|
16
17
|
- a Python-authored service worker with cache-first offline application loading
|
|
18
|
+
- a native Android implementation using the same arithmetic primitive
|
|
17
19
|
- browser tests that interact with the real DOM through button events
|
|
18
20
|
|
|
19
21
|
The example intentionally stays focused on calculator behavior. Reusable widget
|
|
@@ -25,7 +27,7 @@ coverage, including the dropdown menu and floating positioning, lives under
|
|
|
25
27
|
From the repository root:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
28
|
-
|
|
30
|
+
python -m pyjs_examples.calculator
|
|
29
31
|
```
|
|
30
32
|
|
|
31
33
|
Open `http://127.0.0.1:8000/`. Completing a calculation with `=` saves its
|
|
@@ -35,24 +37,38 @@ the server clears them. The app also registers `/calculator-serviceworker.js`.
|
|
|
35
37
|
After its first controlled reload, the calculator can load and calculate while
|
|
36
38
|
the browser is offline.
|
|
37
39
|
|
|
40
|
+
Run the native Android version on an existing emulator, or let the managed
|
|
41
|
+
NativeScript tooling start its configured device:
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
python -m pyjs_examples.calculator --android
|
|
45
|
+
```
|
|
46
|
+
|
|
38
47
|
## Run the tests
|
|
39
48
|
|
|
40
49
|
From the repository root, run the Python target:
|
|
41
50
|
|
|
42
51
|
```bash
|
|
43
|
-
|
|
52
|
+
pytest -q examples/calculator/tests
|
|
44
53
|
```
|
|
45
54
|
|
|
46
55
|
Run the same model tests plus the browser-only UI tests in the browser:
|
|
47
56
|
|
|
48
57
|
```bash
|
|
49
|
-
|
|
58
|
+
pytest -q examples/calculator/tests --pyjs-target=web
|
|
50
59
|
```
|
|
51
60
|
|
|
52
61
|
The pytest plugin starts or reconnects to the browser-test daemon on
|
|
53
62
|
`127.0.0.1:8765`. To keep the daemon alive while iterating:
|
|
54
63
|
|
|
55
64
|
```bash
|
|
56
|
-
|
|
65
|
+
pytest -q examples/calculator/tests --pyjs-target=web \
|
|
57
66
|
--pyjs-daemon-idle-seconds=0
|
|
58
67
|
```
|
|
68
|
+
|
|
69
|
+
Run the Android interaction test with:
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
pytest -q examples/calculator/tests/test_android_calculator.py \
|
|
73
|
+
--pyjs-target=android --pyjs-android-avd=Routine_Test
|
|
74
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""A tested universal calculator for the Web, PWA, and Android targets."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from pyjs.server.dev import serve
|
|
5
|
+
|
|
6
|
+
from .calculator import render, save_result, service_worker_javascript
|
|
7
|
+
|
|
8
|
+
if "--android" in sys.argv:
|
|
9
|
+
from .android import run_android
|
|
10
|
+
|
|
11
|
+
run_android()
|
|
12
|
+
else:
|
|
13
|
+
serve(
|
|
14
|
+
"pyjs_examples.calculator.calculator",
|
|
15
|
+
render=render,
|
|
16
|
+
routes={
|
|
17
|
+
("POST", "/api/calculator/result"): save_result,
|
|
18
|
+
("GET", "/calculator-serviceworker.js"): service_worker_javascript,
|
|
19
|
+
},
|
|
20
|
+
port=int(os.environ.get("PYJS_PORT", "8000")),
|
|
21
|
+
)
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from pyjs import js
|
|
2
|
+
from pyjs.nativescript import (
|
|
3
|
+
Application,
|
|
4
|
+
Button,
|
|
5
|
+
EventData,
|
|
6
|
+
GridLayout,
|
|
7
|
+
Label,
|
|
8
|
+
Page,
|
|
9
|
+
StackLayout,
|
|
10
|
+
)
|
|
11
|
+
from pyjs.tooling.nativescript import NativeScriptProject
|
|
12
|
+
|
|
13
|
+
from .calculator import calculate_result
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@js
|
|
17
|
+
class CalculatorButtonListener:
|
|
18
|
+
def __init__(self, calculator: AndroidCalculator, kind: str, value: str):
|
|
19
|
+
self.calculator = calculator
|
|
20
|
+
self.kind = kind
|
|
21
|
+
self.value = value
|
|
22
|
+
|
|
23
|
+
def handleEvent(self, event: EventData):
|
|
24
|
+
self.calculator.press(self.kind, self.value)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@js
|
|
28
|
+
class AndroidCalculator:
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.display_value = "0"
|
|
31
|
+
self.stored_value = 0.0
|
|
32
|
+
self.pending_operator = ""
|
|
33
|
+
self.replace_display = True
|
|
34
|
+
self.error = False
|
|
35
|
+
|
|
36
|
+
self.display = Label()
|
|
37
|
+
self.display.id = "calculator-display"
|
|
38
|
+
self.display.testID = "calculator-display"
|
|
39
|
+
self.display.text = self.display_value
|
|
40
|
+
self.display.className = (
|
|
41
|
+
"mb-4 rounded-2xl bg-slate-950 p-5 text-right text-5xl "
|
|
42
|
+
"font-bold text-white"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def update_display(self):
|
|
46
|
+
self.display.text = self.display_value
|
|
47
|
+
|
|
48
|
+
def clear(self):
|
|
49
|
+
self.display_value = "0"
|
|
50
|
+
self.stored_value = 0.0
|
|
51
|
+
self.pending_operator = ""
|
|
52
|
+
self.replace_display = True
|
|
53
|
+
self.error = False
|
|
54
|
+
self.update_display()
|
|
55
|
+
|
|
56
|
+
def value(self) -> float:
|
|
57
|
+
if self.error:
|
|
58
|
+
return 0.0
|
|
59
|
+
return float(self.display_value)
|
|
60
|
+
|
|
61
|
+
def input_digit(self, digit: str):
|
|
62
|
+
if self.error:
|
|
63
|
+
self.clear()
|
|
64
|
+
if self.replace_display or self.display_value == "0":
|
|
65
|
+
self.display_value = digit
|
|
66
|
+
self.replace_display = False
|
|
67
|
+
else:
|
|
68
|
+
self.display_value += digit
|
|
69
|
+
self.update_display()
|
|
70
|
+
|
|
71
|
+
def input_decimal(self):
|
|
72
|
+
if self.error:
|
|
73
|
+
self.clear()
|
|
74
|
+
if self.replace_display:
|
|
75
|
+
self.display_value = "0."
|
|
76
|
+
self.replace_display = False
|
|
77
|
+
elif len(self.display_value.split(".")) == 1:
|
|
78
|
+
self.display_value += "."
|
|
79
|
+
self.update_display()
|
|
80
|
+
|
|
81
|
+
def choose_operator(self, operator: str):
|
|
82
|
+
if self.error:
|
|
83
|
+
return
|
|
84
|
+
if self.pending_operator != "" and not self.replace_display:
|
|
85
|
+
self.calculate()
|
|
86
|
+
if self.error:
|
|
87
|
+
return
|
|
88
|
+
self.stored_value = float(self.display_value)
|
|
89
|
+
self.pending_operator = operator
|
|
90
|
+
self.replace_display = True
|
|
91
|
+
|
|
92
|
+
def toggle_sign(self):
|
|
93
|
+
if self.error or self.display_value == "0" or self.display_value == "0.0":
|
|
94
|
+
return
|
|
95
|
+
self.display_value = str(0.0 - float(self.display_value))
|
|
96
|
+
self.update_display()
|
|
97
|
+
|
|
98
|
+
def percent(self):
|
|
99
|
+
if self.error:
|
|
100
|
+
return
|
|
101
|
+
self.display_value = str(float(self.display_value) / 100.0)
|
|
102
|
+
self.replace_display = True
|
|
103
|
+
self.update_display()
|
|
104
|
+
|
|
105
|
+
def calculate(self) -> float:
|
|
106
|
+
operator = self.pending_operator
|
|
107
|
+
if self.error or operator == "":
|
|
108
|
+
return self.value()
|
|
109
|
+
right = float(self.display_value)
|
|
110
|
+
if operator == "/" and right == 0.0:
|
|
111
|
+
self.display_value = "Error"
|
|
112
|
+
self.pending_operator = ""
|
|
113
|
+
self.replace_display = True
|
|
114
|
+
self.error = True
|
|
115
|
+
self.update_display()
|
|
116
|
+
return 0.0
|
|
117
|
+
result = calculate_result(self.stored_value, right, operator)
|
|
118
|
+
self.display_value = str(result)
|
|
119
|
+
self.stored_value = result
|
|
120
|
+
self.pending_operator = ""
|
|
121
|
+
self.replace_display = True
|
|
122
|
+
self.update_display()
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def press(self, kind: str, value: str):
|
|
126
|
+
if kind == "digit":
|
|
127
|
+
self.input_digit(value)
|
|
128
|
+
elif kind == "decimal":
|
|
129
|
+
self.input_decimal()
|
|
130
|
+
elif kind == "operator":
|
|
131
|
+
self.choose_operator(value)
|
|
132
|
+
elif kind == "equals":
|
|
133
|
+
self.calculate()
|
|
134
|
+
elif value == "clear":
|
|
135
|
+
self.clear()
|
|
136
|
+
elif value == "sign":
|
|
137
|
+
self.toggle_sign()
|
|
138
|
+
else:
|
|
139
|
+
self.percent()
|
|
140
|
+
|
|
141
|
+
def key(self, label: str, value: str, kind: str, row: int, column: int) -> Button:
|
|
142
|
+
key = Button()
|
|
143
|
+
key.id = "key-" + value
|
|
144
|
+
key.testID = "key-" + value
|
|
145
|
+
key.text = label
|
|
146
|
+
key.row = row
|
|
147
|
+
key.column = column
|
|
148
|
+
if kind == "operator" or kind == "equals":
|
|
149
|
+
key.className = (
|
|
150
|
+
"m-1 h-14 rounded-xl bg-amber-500 text-2xl font-bold text-white"
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
key.className = (
|
|
154
|
+
"m-1 h-14 rounded-xl bg-slate-100 text-2xl font-semibold "
|
|
155
|
+
"text-slate-950"
|
|
156
|
+
)
|
|
157
|
+
listener = CalculatorButtonListener(self, kind, value)
|
|
158
|
+
key.on(Button.tapEvent, listener.handleEvent)
|
|
159
|
+
return key
|
|
160
|
+
|
|
161
|
+
def page(self) -> Page:
|
|
162
|
+
keypad = GridLayout()
|
|
163
|
+
keypad.rows = "auto,auto,auto,auto,auto"
|
|
164
|
+
keypad.columns = "*,*,*,*"
|
|
165
|
+
labels = [
|
|
166
|
+
("AC", "clear", "clear"),
|
|
167
|
+
("±", "sign", "action"),
|
|
168
|
+
("%", "percent", "action"),
|
|
169
|
+
("÷", "/", "operator"),
|
|
170
|
+
("7", "7", "digit"),
|
|
171
|
+
("8", "8", "digit"),
|
|
172
|
+
("9", "9", "digit"),
|
|
173
|
+
("×", "*", "operator"),
|
|
174
|
+
("4", "4", "digit"),
|
|
175
|
+
("5", "5", "digit"),
|
|
176
|
+
("6", "6", "digit"),
|
|
177
|
+
("−", "-", "operator"),
|
|
178
|
+
("1", "1", "digit"),
|
|
179
|
+
("2", "2", "digit"),
|
|
180
|
+
("3", "3", "digit"),
|
|
181
|
+
("+", "+", "operator"),
|
|
182
|
+
("0", "0", "digit"),
|
|
183
|
+
(".", ".", "decimal"),
|
|
184
|
+
("=", "=", "equals"),
|
|
185
|
+
]
|
|
186
|
+
index = 0
|
|
187
|
+
for label, value, kind in labels:
|
|
188
|
+
keypad.addChild(self.key(label, value, kind, index // 4, index % 4))
|
|
189
|
+
index += 1
|
|
190
|
+
|
|
191
|
+
content = StackLayout()
|
|
192
|
+
content.className = "bg-slate-900 p-5"
|
|
193
|
+
content.verticalAlignment = "middle"
|
|
194
|
+
title = Label()
|
|
195
|
+
title.text = "PyJS Calculator"
|
|
196
|
+
title.className = "mb-4 text-center text-2xl font-bold text-sky-300"
|
|
197
|
+
content.addChild(title)
|
|
198
|
+
content.addChild(self.display)
|
|
199
|
+
content.addChild(keypad)
|
|
200
|
+
|
|
201
|
+
page = Page()
|
|
202
|
+
page.actionBarHidden = True
|
|
203
|
+
page.className = "bg-slate-900"
|
|
204
|
+
page.content = content
|
|
205
|
+
return page
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@js
|
|
209
|
+
def create_android_page() -> Page:
|
|
210
|
+
return AndroidCalculator().page()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@js
|
|
214
|
+
def android_main():
|
|
215
|
+
Application.run(create_android_page)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def run_android():
|
|
219
|
+
project = NativeScriptProject(
|
|
220
|
+
android_main,
|
|
221
|
+
name="PyJS Calculator",
|
|
222
|
+
app_id="org.pyjs.calculator",
|
|
223
|
+
)
|
|
224
|
+
device = project.run_android()
|
|
225
|
+
print(f"PyJS Calculator is running on {device.identifier}.")
|
|
@@ -12,6 +12,17 @@ from pyjs.web.serviceworker import (
|
|
|
12
12
|
from pyjs.web.ui import Widget
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
@js
|
|
16
|
+
def calculate_result(left: float, right: float, operator: str) -> float:
|
|
17
|
+
if operator == "+":
|
|
18
|
+
return left + right
|
|
19
|
+
if operator == "-":
|
|
20
|
+
return left - right
|
|
21
|
+
if operator == "*":
|
|
22
|
+
return left * right
|
|
23
|
+
return left / right
|
|
24
|
+
|
|
25
|
+
|
|
15
26
|
@js
|
|
16
27
|
async def install_app_shell(cache_name: str, assets: list[str]):
|
|
17
28
|
cache = await caches.open(cache_name)
|
|
@@ -41,14 +52,12 @@ class InstallHandler:
|
|
|
41
52
|
self.cache_name = cache_name
|
|
42
53
|
self.assets = assets
|
|
43
54
|
|
|
44
|
-
@js(include=True)
|
|
45
55
|
def handleEvent(self, event: ExtendableEvent):
|
|
46
56
|
event.waitUntil(install_app_shell(self.cache_name, self.assets))
|
|
47
57
|
|
|
48
58
|
|
|
49
59
|
@js
|
|
50
60
|
class ActivateHandler:
|
|
51
|
-
@js(include=True)
|
|
52
61
|
def handleEvent(self, event: ExtendableEvent):
|
|
53
62
|
event.waitUntil(activate_app_shell())
|
|
54
63
|
|
|
@@ -58,7 +67,6 @@ class FetchHandler:
|
|
|
58
67
|
def __init__(self, cache_name: str):
|
|
59
68
|
self.cache_name = cache_name
|
|
60
69
|
|
|
61
|
-
@js(include=True)
|
|
62
70
|
def handleEvent(self, event: FetchEvent):
|
|
63
71
|
if event.request.method == "GET":
|
|
64
72
|
event.respondWith(cache_first(event.request, self.cache_name))
|
|
@@ -75,11 +83,11 @@ def service_worker_main():
|
|
|
75
83
|
|
|
76
84
|
@nojs
|
|
77
85
|
def service_worker_javascript(request):
|
|
78
|
-
from pyjs.server import
|
|
86
|
+
from pyjs.server.dev import Response
|
|
79
87
|
from pyjs.transpiler import bundle
|
|
80
88
|
|
|
81
89
|
source, _ = bundle(service_worker_main, include_main=True, include_css=False)
|
|
82
|
-
return
|
|
90
|
+
return Response(
|
|
83
91
|
source,
|
|
84
92
|
content_type="application/javascript",
|
|
85
93
|
headers={
|
|
@@ -96,7 +104,6 @@ class CalculatorKeyListener:
|
|
|
96
104
|
self.kind = kind
|
|
97
105
|
self.value = value
|
|
98
106
|
|
|
99
|
-
@js(include=True)
|
|
100
107
|
async def handleEvent(self, event: Event):
|
|
101
108
|
self.app.press(self.kind, self.value)
|
|
102
109
|
if self.kind == "equals" and not self.app.error:
|
|
@@ -334,15 +341,6 @@ class CalculatorApp(Widget):
|
|
|
334
341
|
self.pending_operator = operator
|
|
335
342
|
self.replace_display = True
|
|
336
343
|
|
|
337
|
-
def calculate_result(self, left: float, right: float, operator: str) -> float:
|
|
338
|
-
if operator == "+":
|
|
339
|
-
return left + right
|
|
340
|
-
if operator == "-":
|
|
341
|
-
return left - right
|
|
342
|
-
if operator == "*":
|
|
343
|
-
return left * right
|
|
344
|
-
return left / right
|
|
345
|
-
|
|
346
344
|
def calculate(self) -> float:
|
|
347
345
|
operator = self.pending_operator
|
|
348
346
|
if self.error or operator == "":
|
|
@@ -354,7 +352,7 @@ class CalculatorApp(Widget):
|
|
|
354
352
|
self.replace_display = True
|
|
355
353
|
self.error = True
|
|
356
354
|
return 0.0
|
|
357
|
-
result =
|
|
355
|
+
result = calculate_result(self.stored_value, right, operator)
|
|
358
356
|
self.display_value = str(result)
|
|
359
357
|
self.stored_value = result
|
|
360
358
|
self.pending_operator = ""
|
|
@@ -427,7 +425,7 @@ def save_result(request):
|
|
|
427
425
|
if __name__ == "__main__":
|
|
428
426
|
import os
|
|
429
427
|
|
|
430
|
-
from pyjs.server import serve
|
|
428
|
+
from pyjs.server.dev import serve
|
|
431
429
|
|
|
432
430
|
serve(
|
|
433
431
|
"calculator",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from pyjs.testing.android import AndroidTestSession
|
|
5
|
+
from pyjs.tooling.android import AndroidSDK
|
|
6
|
+
from pyjs.tooling.nativescript import NativeScriptProject
|
|
7
|
+
from pyjs_examples.calculator.android import android_main
|
|
8
|
+
|
|
9
|
+
pytestmark = pytest.mark.pyjs_android
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_calculator_runs_on_android(request, pyjs_artifacts_dir: Path):
|
|
13
|
+
android = AndroidSDK()
|
|
14
|
+
project = NativeScriptProject(
|
|
15
|
+
android_main,
|
|
16
|
+
name="PyJS Calculator",
|
|
17
|
+
app_id="org.pyjs.calculator",
|
|
18
|
+
android=android,
|
|
19
|
+
)
|
|
20
|
+
device = project.run_android(request.config.getoption("--pyjs-android-avd"))
|
|
21
|
+
session = AndroidTestSession(android, device, project.app_id)
|
|
22
|
+
artifacts = pyjs_artifacts_dir / "android"
|
|
23
|
+
|
|
24
|
+
with session.artifacts_on_failure(artifacts, "calculator"):
|
|
25
|
+
session.launch()
|
|
26
|
+
session.wait_for_element(test_id="calculator-display", text="0")
|
|
27
|
+
session.tap_element(test_id="key-1")
|
|
28
|
+
session.tap_element(test_id="key-+")
|
|
29
|
+
session.tap_element(test_id="key-2")
|
|
30
|
+
session.tap_element(test_id="key-=")
|
|
31
|
+
session.wait_for_element(test_id="calculator-display", text="3.0")
|
|
32
|
+
assert session.log_errors() == ""
|
|
33
|
+
|
|
34
|
+
session.capture_artifacts(artifacts, "calculator")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pyjs_examples.calculator.android import AndroidCalculator
|
|
3
|
+
|
|
4
|
+
pytestmark = pytest.mark.pyjs_python
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_android_calculator_uses_the_same_arithmetic_behavior():
|
|
8
|
+
calculator = AndroidCalculator()
|
|
9
|
+
|
|
10
|
+
calculator.input_digit("1")
|
|
11
|
+
calculator.input_digit("2")
|
|
12
|
+
calculator.choose_operator("+")
|
|
13
|
+
calculator.input_digit("7")
|
|
14
|
+
|
|
15
|
+
assert calculator.calculate() == 19.0
|
|
16
|
+
assert calculator.display.text == "19.0"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_android_calculator_builds_a_complete_native_keypad():
|
|
20
|
+
calculator = AndroidCalculator()
|
|
21
|
+
page = calculator.page()
|
|
22
|
+
|
|
23
|
+
assert page.content.getViewById("calculator-display") is calculator.display
|
|
24
|
+
assert page.content.getViewById("key-clear") is not None
|
|
25
|
+
assert page.content.getViewById("key-+") is not None
|
|
26
|
+
assert page.content.getViewById("key-=") is not None
|