pyjs-examples 0.0.2__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.
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/PKG-INFO +33 -23
- pyjs_examples-0.3.0/README.md +54 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/calculator.py +4 -4
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/chatroom.py +51 -32
- pyjs_examples-0.3.0/issue_tracker/README.md +45 -0
- pyjs_examples-0.3.0/issue_tracker/__init__.py +1 -0
- pyjs_examples-0.3.0/issue_tracker/__main__.py +46 -0
- pyjs_examples-0.3.0/issue_tracker/api.py +353 -0
- pyjs_examples-0.3.0/issue_tracker/apps.py +7 -0
- pyjs_examples-0.3.0/issue_tracker/core.py +130 -0
- pyjs_examples-0.3.0/issue_tracker/fixtures/issue_tracker.json +218 -0
- pyjs_examples-0.3.0/issue_tracker/migrations/0001_initial.py +103 -0
- pyjs_examples-0.3.0/issue_tracker/migrations/__init__.py +0 -0
- pyjs_examples-0.3.0/issue_tracker/models.py +140 -0
- pyjs_examples-0.3.0/issue_tracker/server.py +37 -0
- pyjs_examples-0.3.0/issue_tracker/settings.py +22 -0
- pyjs_examples-0.3.0/issue_tracker/tests/conftest.py +27 -0
- pyjs_examples-0.3.0/issue_tracker/tests/test_browser_issue_tracker.py +204 -0
- pyjs_examples-0.3.0/issue_tracker/tests/test_issue_tracker.py +295 -0
- pyjs_examples-0.3.0/issue_tracker/ui.py +742 -0
- pyjs_examples-0.3.0/issue_tracker/urls.py +10 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/PKG-INFO +33 -23
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/SOURCES.txt +34 -0
- pyjs_examples-0.3.0/pyjs_examples.egg-info/requires.txt +9 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyproject.toml +15 -2
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/space_invaders.py +3 -4
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/tests/test_browser_todomvc.py +2 -1
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/todomvc.py +4 -6
- pyjs_examples-0.0.2/README.md +0 -48
- pyjs_examples-0.0.2/pyjs_examples.egg-info/requires.txt +0 -4
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/tests/test_browser_calculator.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/tests/test_calculator.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/tests/test_pwa.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/catalog.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/chat_server.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/tests/test_browser_chatroom.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/tests/test_chat_server.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/visual_test.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/counter.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/kanban.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/tests/renders/kanban-board.png +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/tests/test_browser_kanban.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/tests/test_kanban.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/visual_test.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/counter.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/tests/test_android_counter.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/tests/test_counter.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/navigation.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/tests/test_browser_navigation.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/tests/test_navigation.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/dependency_links.txt +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/entry_points.txt +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/top_level.txt +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/state_counter.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/tests/test_browser_state_counter.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/tests/test_state_counter.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/tests/test_browser_ui_dialog.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/tests/test_ui_dialog.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/ui_dialog.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/setup.cfg +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/tests/renders/action_scene.png +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/tests/renders/initial_wave.png +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/tests/test_browser_game.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/tests/test_game.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/base.css +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/index.css +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/tests/test_todomvc.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/README.md +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/__init__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/__main__.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/tests/test_browser_workspace.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/tests/test_workspace.py +0 -0
- {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyjs-examples
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Official example applications for PyJS.
|
|
5
5
|
Author-email: Lex Berezhny <lex@damoti.com>
|
|
6
6
|
License-Expression: BSD-3-Clause
|
|
@@ -18,15 +18,20 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: pyjs==0.0
|
|
21
|
+
Requires-Dist: pyjs==0.3.0
|
|
22
|
+
Provides-Extra: issue-tracker
|
|
23
|
+
Requires-Dist: django<6.2,>=6.0; extra == "issue-tracker"
|
|
24
|
+
Requires-Dist: pydantic<3,>=2; extra == "issue-tracker"
|
|
25
|
+
Requires-Dist: uvicorn<1,>=0.52; extra == "issue-tracker"
|
|
22
26
|
Provides-Extra: test
|
|
23
27
|
Requires-Dist: pytest>=9.0; extra == "test"
|
|
24
28
|
|
|
25
29
|
# PyJS Examples
|
|
26
30
|
|
|
27
|
-
This directory is the source tree for the separately installable
|
|
28
|
-
Setuptools maps this directory to the
|
|
29
|
-
|
|
31
|
+
This directory is the source tree for the separately installable
|
|
32
|
+
`pyjs-examples` distribution. Setuptools maps this directory to the
|
|
33
|
+
`pyjs_examples` import package, so the examples remain easy to browse in the
|
|
34
|
+
repository while installing as an ordinary top-level package.
|
|
30
35
|
|
|
31
36
|
From the repository root, install the core and examples from a source checkout:
|
|
32
37
|
|
|
@@ -34,7 +39,8 @@ From the repository root, install the core and examples from a source checkout:
|
|
|
34
39
|
uv sync
|
|
35
40
|
```
|
|
36
41
|
|
|
37
|
-
An installed PyJS release provides the same package through the `examples`
|
|
42
|
+
An installed PyJS release provides the same package through the `examples`
|
|
43
|
+
extra:
|
|
38
44
|
|
|
39
45
|
```bash
|
|
40
46
|
python -m pip install "pyjs[examples]"
|
|
@@ -42,17 +48,20 @@ python -m pip install "pyjs[examples]"
|
|
|
42
48
|
|
|
43
49
|
Run each complete application with the command shown:
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
- [Calculator](calculator/): `python -m pyjs_examples.calculator`
|
|
52
|
+
- [Chatroom](chatroom/): `python -m pyjs_examples.chatroom`
|
|
53
|
+
- [Django issue tracker](issue_tracker/):
|
|
54
|
+
`python -m pyjs_examples.issue_tracker`
|
|
55
|
+
- [Kanban board](kanban/): `python -m pyjs_examples.kanban`
|
|
56
|
+
- [Navigation workspace](navigation/):
|
|
57
|
+
`python -m pyjs_examples.navigation`
|
|
58
|
+
- [NativeScript Counter](nativescript_counter/):
|
|
59
|
+
`python -m pyjs_examples.nativescript_counter`
|
|
60
|
+
- [Space Invaders](space_invaders/):
|
|
61
|
+
`python -m pyjs_examples.space_invaders`
|
|
62
|
+
- [TodoMVC](todomvc/): `python -m pyjs_examples.todomvc`
|
|
63
|
+
- [Component workspace](workspace/): `python -m pyjs_examples.workspace`
|
|
64
|
+
- [Component catalog](catalog.py): `python -m pyjs_examples.catalog`
|
|
56
65
|
|
|
57
66
|
List or copy examples using the core scaffolding command:
|
|
58
67
|
|
|
@@ -63,10 +72,11 @@ pyjs create my-calculator --template pyjs_examples.calculator
|
|
|
63
72
|
pyjs create --single-file my_counter.py --template pyjs_examples.counter
|
|
64
73
|
```
|
|
65
74
|
|
|
66
|
-
The distribution registers modules and packages through the `pyjs.templates`
|
|
67
|
-
PyJS infers file and directory templates from normal Python
|
|
68
|
-
a catalog.
|
|
75
|
+
The distribution registers modules and packages through the `pyjs.templates`
|
|
76
|
+
entry-point group. PyJS infers file and directory templates from normal Python
|
|
77
|
+
module structure instead of hardcoding a catalog.
|
|
69
78
|
|
|
70
|
-
The [focused samples](samples/) are small, executable sources intended for
|
|
71
|
-
and `pyjs create PROJECT --template NAME`. The initial
|
|
72
|
-
`pyjs_examples.samples.state_counter` and
|
|
79
|
+
The [focused samples](samples/) are small, executable sources intended for
|
|
80
|
+
documentation excerpts and `pyjs create PROJECT --template NAME`. The initial
|
|
81
|
+
catalog includes `pyjs_examples.samples.state_counter` and
|
|
82
|
+
`pyjs_examples.samples.ui_dialog`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# PyJS Examples
|
|
2
|
+
|
|
3
|
+
This directory is the source tree for the separately installable
|
|
4
|
+
`pyjs-examples` distribution. Setuptools maps this directory to the
|
|
5
|
+
`pyjs_examples` import package, so the examples remain easy to browse in the
|
|
6
|
+
repository while installing as an ordinary top-level package.
|
|
7
|
+
|
|
8
|
+
From the repository root, install the core and examples from a source checkout:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv sync
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
An installed PyJS release provides the same package through the `examples`
|
|
15
|
+
extra:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
python -m pip install "pyjs[examples]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run each complete application with the command shown:
|
|
22
|
+
|
|
23
|
+
- [Calculator](calculator/): `python -m pyjs_examples.calculator`
|
|
24
|
+
- [Chatroom](chatroom/): `python -m pyjs_examples.chatroom`
|
|
25
|
+
- [Django issue tracker](issue_tracker/):
|
|
26
|
+
`python -m pyjs_examples.issue_tracker`
|
|
27
|
+
- [Kanban board](kanban/): `python -m pyjs_examples.kanban`
|
|
28
|
+
- [Navigation workspace](navigation/):
|
|
29
|
+
`python -m pyjs_examples.navigation`
|
|
30
|
+
- [NativeScript Counter](nativescript_counter/):
|
|
31
|
+
`python -m pyjs_examples.nativescript_counter`
|
|
32
|
+
- [Space Invaders](space_invaders/):
|
|
33
|
+
`python -m pyjs_examples.space_invaders`
|
|
34
|
+
- [TodoMVC](todomvc/): `python -m pyjs_examples.todomvc`
|
|
35
|
+
- [Component workspace](workspace/): `python -m pyjs_examples.workspace`
|
|
36
|
+
- [Component catalog](catalog.py): `python -m pyjs_examples.catalog`
|
|
37
|
+
|
|
38
|
+
List or copy examples using the core scaffolding command:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pyjs template
|
|
42
|
+
pyjs template pyjs_examples.calculator
|
|
43
|
+
pyjs create my-calculator --template pyjs_examples.calculator
|
|
44
|
+
pyjs create --single-file my_counter.py --template pyjs_examples.counter
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The distribution registers modules and packages through the `pyjs.templates`
|
|
48
|
+
entry-point group. PyJS infers file and directory templates from normal Python
|
|
49
|
+
module structure instead of hardcoding a catalog.
|
|
50
|
+
|
|
51
|
+
The [focused samples](samples/) are small, executable sources intended for
|
|
52
|
+
documentation excerpts and `pyjs create PROJECT --template NAME`. The initial
|
|
53
|
+
catalog includes `pyjs_examples.samples.state_counter` and
|
|
54
|
+
`pyjs_examples.samples.ui_dialog`.
|
|
@@ -41,14 +41,14 @@ class InstallHandler:
|
|
|
41
41
|
self.cache_name = cache_name
|
|
42
42
|
self.assets = assets
|
|
43
43
|
|
|
44
|
-
@js
|
|
44
|
+
@js.keep
|
|
45
45
|
def handleEvent(self, event: ExtendableEvent):
|
|
46
46
|
event.waitUntil(install_app_shell(self.cache_name, self.assets))
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
@js
|
|
50
50
|
class ActivateHandler:
|
|
51
|
-
@js
|
|
51
|
+
@js.keep
|
|
52
52
|
def handleEvent(self, event: ExtendableEvent):
|
|
53
53
|
event.waitUntil(activate_app_shell())
|
|
54
54
|
|
|
@@ -58,7 +58,7 @@ class FetchHandler:
|
|
|
58
58
|
def __init__(self, cache_name: str):
|
|
59
59
|
self.cache_name = cache_name
|
|
60
60
|
|
|
61
|
-
@js
|
|
61
|
+
@js.keep
|
|
62
62
|
def handleEvent(self, event: FetchEvent):
|
|
63
63
|
if event.request.method == "GET":
|
|
64
64
|
event.respondWith(cache_first(event.request, self.cache_name))
|
|
@@ -96,7 +96,7 @@ class CalculatorKeyListener:
|
|
|
96
96
|
self.kind = kind
|
|
97
97
|
self.value = value
|
|
98
98
|
|
|
99
|
-
@js
|
|
99
|
+
@js.keep
|
|
100
100
|
async def handleEvent(self, event: Event):
|
|
101
101
|
self.app.press(self.kind, self.value)
|
|
102
102
|
if self.kind == "equals" and not self.app.error:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import cast
|
|
2
2
|
|
|
3
3
|
from pyjs import effect, js, nojs, state
|
|
4
|
-
from pyjs.javascript import JSON
|
|
4
|
+
from pyjs.javascript import JSON, JSONObject
|
|
5
5
|
from pyjs.web.dom import location, sessionStorage
|
|
6
6
|
from pyjs.web.domx import button, form, input, ref, tag, tw
|
|
7
7
|
from pyjs.web.events import Event, MessageEvent
|
|
@@ -9,7 +9,7 @@ from pyjs.web.network import WebSocket
|
|
|
9
9
|
from pyjs.web.ui import Widget
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class ChatMessage(
|
|
12
|
+
class ChatMessage(JSONObject):
|
|
13
13
|
id: int
|
|
14
14
|
room: str
|
|
15
15
|
sender: str
|
|
@@ -17,7 +17,7 @@ class ChatMessage(TypedDict):
|
|
|
17
17
|
time: str
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class ChatSnapshot(
|
|
20
|
+
class ChatSnapshot(JSONObject):
|
|
21
21
|
type: str
|
|
22
22
|
room: str
|
|
23
23
|
rooms: list[str]
|
|
@@ -26,13 +26,11 @@ class ChatSnapshot(TypedDict):
|
|
|
26
26
|
message: str
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
self.room = room
|
|
35
|
-
self.text = text
|
|
29
|
+
class OutgoingChatMessage(JSONObject):
|
|
30
|
+
type: str
|
|
31
|
+
name: str
|
|
32
|
+
room: str
|
|
33
|
+
text: str
|
|
36
34
|
|
|
37
35
|
|
|
38
36
|
@js
|
|
@@ -40,7 +38,7 @@ class NameSubmitListener:
|
|
|
40
38
|
def __init__(self, app: ChatRoomApp):
|
|
41
39
|
self.app = app
|
|
42
40
|
|
|
43
|
-
@js
|
|
41
|
+
@js.keep
|
|
44
42
|
def handleEvent(self, event: Event):
|
|
45
43
|
event.preventDefault()
|
|
46
44
|
self.app.submit_name()
|
|
@@ -51,7 +49,7 @@ class RoomCreateListener:
|
|
|
51
49
|
def __init__(self, app: ChatRoomApp):
|
|
52
50
|
self.app = app
|
|
53
51
|
|
|
54
|
-
@js
|
|
52
|
+
@js.keep
|
|
55
53
|
def handleEvent(self, event: Event):
|
|
56
54
|
event.preventDefault()
|
|
57
55
|
self.app.create_room()
|
|
@@ -62,7 +60,7 @@ class MessageSubmitListener:
|
|
|
62
60
|
def __init__(self, app: ChatRoomApp):
|
|
63
61
|
self.app = app
|
|
64
62
|
|
|
65
|
-
@js
|
|
63
|
+
@js.keep
|
|
66
64
|
def handleEvent(self, event: Event):
|
|
67
65
|
event.preventDefault()
|
|
68
66
|
self.app.submit_message()
|
|
@@ -74,7 +72,7 @@ class RoomClickListener:
|
|
|
74
72
|
self.app = app
|
|
75
73
|
self.room = room
|
|
76
74
|
|
|
77
|
-
@js
|
|
75
|
+
@js.keep
|
|
78
76
|
def handleEvent(self, event: Event):
|
|
79
77
|
self.app.switch_room(self.room)
|
|
80
78
|
|
|
@@ -84,7 +82,7 @@ class ReconnectClickListener:
|
|
|
84
82
|
def __init__(self, app: ChatRoomApp):
|
|
85
83
|
self.app = app
|
|
86
84
|
|
|
87
|
-
@js
|
|
85
|
+
@js.keep
|
|
88
86
|
def handleEvent(self, event: Event):
|
|
89
87
|
self.app.connect()
|
|
90
88
|
|
|
@@ -94,7 +92,7 @@ class SignOutClickListener:
|
|
|
94
92
|
def __init__(self, app: ChatRoomApp):
|
|
95
93
|
self.app = app
|
|
96
94
|
|
|
97
|
-
@js
|
|
95
|
+
@js.keep
|
|
98
96
|
def handleEvent(self, event: Event):
|
|
99
97
|
self.app.sign_out()
|
|
100
98
|
|
|
@@ -104,7 +102,7 @@ class SocketOpenListener:
|
|
|
104
102
|
def __init__(self, app: ChatRoomApp):
|
|
105
103
|
self.app = app
|
|
106
104
|
|
|
107
|
-
@js
|
|
105
|
+
@js.keep
|
|
108
106
|
def handleEvent(self, event: Event):
|
|
109
107
|
self.app.socket_opened()
|
|
110
108
|
|
|
@@ -114,9 +112,9 @@ class SocketMessageListener:
|
|
|
114
112
|
def __init__(self, app: ChatRoomApp):
|
|
115
113
|
self.app = app
|
|
116
114
|
|
|
117
|
-
@js
|
|
115
|
+
@js.keep
|
|
118
116
|
def handleEvent(self, event: MessageEvent):
|
|
119
|
-
message: ChatSnapshot = JSON.parse(event.data)
|
|
117
|
+
message: ChatSnapshot = JSON.parse(cast(str, event.data))
|
|
120
118
|
self.app.receive(message)
|
|
121
119
|
|
|
122
120
|
|
|
@@ -125,7 +123,7 @@ class SocketCloseListener:
|
|
|
125
123
|
def __init__(self, app: ChatRoomApp):
|
|
126
124
|
self.app = app
|
|
127
125
|
|
|
128
|
-
@js
|
|
126
|
+
@js.keep
|
|
129
127
|
def handleEvent(self, event: Event):
|
|
130
128
|
self.app.socket_closed()
|
|
131
129
|
|
|
@@ -135,7 +133,7 @@ class SocketErrorListener:
|
|
|
135
133
|
def __init__(self, app: ChatRoomApp):
|
|
136
134
|
self.app = app
|
|
137
135
|
|
|
138
|
-
@js
|
|
136
|
+
@js.keep
|
|
139
137
|
def handleEvent(self, event: Event):
|
|
140
138
|
self.app.show_error("The chat server could not be reached.")
|
|
141
139
|
|
|
@@ -494,10 +492,9 @@ class ChatRoomApp(Widget):
|
|
|
494
492
|
|
|
495
493
|
def sign_out(self):
|
|
496
494
|
sessionStorage.removeItem("pyjs-chatroom-display-name")
|
|
497
|
-
socket = self.socket
|
|
498
495
|
if self.connection == "connected":
|
|
499
|
-
if socket is not None:
|
|
500
|
-
socket.close()
|
|
496
|
+
if self.socket is not None:
|
|
497
|
+
self.socket.close()
|
|
501
498
|
self.name = ""
|
|
502
499
|
self.name_input.value = ""
|
|
503
500
|
self.show_sign_in()
|
|
@@ -516,7 +513,9 @@ class ChatRoomApp(Widget):
|
|
|
516
513
|
|
|
517
514
|
def socket_opened(self):
|
|
518
515
|
self.connection = "connected"
|
|
519
|
-
self.send(
|
|
516
|
+
self.send(
|
|
517
|
+
OutgoingChatMessage(type="join", name=self.name, room=self.room, text="")
|
|
518
|
+
)
|
|
520
519
|
|
|
521
520
|
def socket_closed(self):
|
|
522
521
|
if self.connection == "signed-out":
|
|
@@ -524,10 +523,9 @@ class ChatRoomApp(Widget):
|
|
|
524
523
|
self.connection = "disconnected"
|
|
525
524
|
|
|
526
525
|
def send(self, message: OutgoingChatMessage):
|
|
527
|
-
socket = self.socket
|
|
528
526
|
if self.connection == "connected":
|
|
529
|
-
if socket is not None:
|
|
530
|
-
socket.send(JSON.stringify(message))
|
|
527
|
+
if self.socket is not None:
|
|
528
|
+
self.socket.send(JSON.stringify(message))
|
|
531
529
|
|
|
532
530
|
def receive(self, message: ChatSnapshot):
|
|
533
531
|
if message["type"] == "error":
|
|
@@ -654,21 +652,42 @@ class ChatRoomApp(Widget):
|
|
|
654
652
|
)
|
|
655
653
|
|
|
656
654
|
def switch_room(self, room: str):
|
|
657
|
-
self.send(
|
|
655
|
+
self.send(
|
|
656
|
+
OutgoingChatMessage(
|
|
657
|
+
type="switch_room",
|
|
658
|
+
name=self.name,
|
|
659
|
+
room=room,
|
|
660
|
+
text="",
|
|
661
|
+
)
|
|
662
|
+
)
|
|
658
663
|
|
|
659
664
|
def create_room(self):
|
|
660
665
|
room = self.room_input.value.strip()
|
|
661
666
|
if room == "":
|
|
662
667
|
return
|
|
663
668
|
self.room_input.value = ""
|
|
664
|
-
self.send(
|
|
669
|
+
self.send(
|
|
670
|
+
OutgoingChatMessage(
|
|
671
|
+
type="create_room",
|
|
672
|
+
name=self.name,
|
|
673
|
+
room=room,
|
|
674
|
+
text="",
|
|
675
|
+
)
|
|
676
|
+
)
|
|
665
677
|
|
|
666
678
|
def submit_message(self):
|
|
667
679
|
text = self.message_input.value.strip()
|
|
668
680
|
if text == "":
|
|
669
681
|
return
|
|
670
682
|
self.message_input.value = ""
|
|
671
|
-
self.send(
|
|
683
|
+
self.send(
|
|
684
|
+
OutgoingChatMessage(
|
|
685
|
+
type="message",
|
|
686
|
+
name=self.name,
|
|
687
|
+
room=self.room,
|
|
688
|
+
text=text,
|
|
689
|
+
)
|
|
690
|
+
)
|
|
672
691
|
|
|
673
692
|
def show_error(self, message: str):
|
|
674
693
|
self.error_banner.textContent = message
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Django issue tracker
|
|
2
|
+
|
|
3
|
+
This conventional Django app uses models and relations for projects, issues,
|
|
4
|
+
users, labels, milestones, comments, and activity. Shared Pydantic models form
|
|
5
|
+
the typed client/server boundary. The app demonstrates SSR, generated client
|
|
6
|
+
API proxies, nested resources, Django request validation, filtering, mutations,
|
|
7
|
+
and model-driven forms.
|
|
8
|
+
|
|
9
|
+
From the repository root, run:
|
|
10
|
+
|
|
11
|
+
```console
|
|
12
|
+
uv run python -m pyjs_examples.issue_tracker
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For an installed examples package, install its optional dependencies first:
|
|
16
|
+
|
|
17
|
+
```console
|
|
18
|
+
pip install "pyjs-examples[issue_tracker]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Open the printed URL. Create an issue, filter the list, add a comment, and close
|
|
22
|
+
the issue. Every operation passes through the generated typed client and the
|
|
23
|
+
Django adapter. The standalone runner migrates a temporary SQLite database and
|
|
24
|
+
loads `fixtures/issue_tracker.json`, so each server process starts with the same
|
|
25
|
+
sample project and removes its database when it stops.
|
|
26
|
+
|
|
27
|
+
The application is divided by runtime responsibility:
|
|
28
|
+
|
|
29
|
+
- `core.py` contains Pydantic values shared by the server and client;
|
|
30
|
+
- `api.py` declares shared resources and supplies their ORM-backed server
|
|
31
|
+
implementations;
|
|
32
|
+
- `ui.py` contains the transpiled and server-rendered interface; and
|
|
33
|
+
- `server.py` connects the interface and API to Django HTTP views.
|
|
34
|
+
|
|
35
|
+
The Django persistence models remain in `models.py`, as they would in an
|
|
36
|
+
ordinary Django application.
|
|
37
|
+
|
|
38
|
+
The standalone app is served through Django's ASGI interface. Open it in two
|
|
39
|
+
browser windows and create or update an issue in either one. A shared
|
|
40
|
+
`LiveSession` receives resource invalidations over SSE, batches the affected
|
|
41
|
+
typed operations into one registered subscription, and updates the other
|
|
42
|
+
window without a page refresh. Django evaluates the registered operations with
|
|
43
|
+
the current SSE request and includes replacement values in the event. Each
|
|
44
|
+
watched query retains its generated API operation and last submitted request,
|
|
45
|
+
so live updates do not read partially edited form or filter controls.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""A typed Django issue tracker built with PyJS."""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import django
|
|
6
|
+
import uvicorn
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.core.asgi import get_asgi_application
|
|
9
|
+
from django.core.management import call_command
|
|
10
|
+
from django.db import connections
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
os.environ.setdefault(
|
|
15
|
+
"DJANGO_SETTINGS_MODULE",
|
|
16
|
+
"pyjs_examples.issue_tracker.settings",
|
|
17
|
+
)
|
|
18
|
+
django.setup()
|
|
19
|
+
|
|
20
|
+
from pyjs_examples.issue_tracker.server import STATIC_ROOT, application
|
|
21
|
+
|
|
22
|
+
database_path = Path(settings.DATABASES["default"]["NAME"])
|
|
23
|
+
database_path.unlink(missing_ok=True)
|
|
24
|
+
call_command("migrate", interactive=False, verbosity=0)
|
|
25
|
+
call_command("loaddata", "issue_tracker", verbosity=0)
|
|
26
|
+
application.build(STATIC_ROOT / "issue_tracker")
|
|
27
|
+
|
|
28
|
+
host = "127.0.0.1"
|
|
29
|
+
port = int(os.environ.get("PYJS_PORT", "8000"))
|
|
30
|
+
url = f"http://{host}:{port}/"
|
|
31
|
+
print(f"Serving the PyJS issue tracker at {url}")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
uvicorn.run(
|
|
35
|
+
get_asgi_application(),
|
|
36
|
+
host=host,
|
|
37
|
+
port=port,
|
|
38
|
+
log_level="warning",
|
|
39
|
+
)
|
|
40
|
+
finally:
|
|
41
|
+
connections.close_all()
|
|
42
|
+
database_path.unlink(missing_ok=True)
|
|
43
|
+
shutil.rmtree(STATIC_ROOT, ignore_errors=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
main()
|