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.
Files changed (102) hide show
  1. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/PKG-INFO +33 -23
  2. pyjs_examples-0.3.0/README.md +54 -0
  3. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/calculator.py +4 -4
  4. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/chatroom.py +51 -32
  5. pyjs_examples-0.3.0/issue_tracker/README.md +45 -0
  6. pyjs_examples-0.3.0/issue_tracker/__init__.py +1 -0
  7. pyjs_examples-0.3.0/issue_tracker/__main__.py +46 -0
  8. pyjs_examples-0.3.0/issue_tracker/api.py +353 -0
  9. pyjs_examples-0.3.0/issue_tracker/apps.py +7 -0
  10. pyjs_examples-0.3.0/issue_tracker/core.py +130 -0
  11. pyjs_examples-0.3.0/issue_tracker/fixtures/issue_tracker.json +218 -0
  12. pyjs_examples-0.3.0/issue_tracker/migrations/0001_initial.py +103 -0
  13. pyjs_examples-0.3.0/issue_tracker/migrations/__init__.py +0 -0
  14. pyjs_examples-0.3.0/issue_tracker/models.py +140 -0
  15. pyjs_examples-0.3.0/issue_tracker/server.py +37 -0
  16. pyjs_examples-0.3.0/issue_tracker/settings.py +22 -0
  17. pyjs_examples-0.3.0/issue_tracker/tests/conftest.py +27 -0
  18. pyjs_examples-0.3.0/issue_tracker/tests/test_browser_issue_tracker.py +204 -0
  19. pyjs_examples-0.3.0/issue_tracker/tests/test_issue_tracker.py +295 -0
  20. pyjs_examples-0.3.0/issue_tracker/ui.py +742 -0
  21. pyjs_examples-0.3.0/issue_tracker/urls.py +10 -0
  22. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/PKG-INFO +33 -23
  23. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/SOURCES.txt +34 -0
  24. pyjs_examples-0.3.0/pyjs_examples.egg-info/requires.txt +9 -0
  25. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyproject.toml +15 -2
  26. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/space_invaders.py +3 -4
  27. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/tests/test_browser_todomvc.py +2 -1
  28. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/todomvc.py +4 -6
  29. pyjs_examples-0.0.2/README.md +0 -48
  30. pyjs_examples-0.0.2/pyjs_examples.egg-info/requires.txt +0 -4
  31. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/__init__.py +0 -0
  32. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/README.md +0 -0
  33. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/__init__.py +0 -0
  34. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/__main__.py +0 -0
  35. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/tests/test_browser_calculator.py +0 -0
  36. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/tests/test_calculator.py +0 -0
  37. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/calculator/tests/test_pwa.py +0 -0
  38. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/catalog.py +0 -0
  39. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/README.md +0 -0
  40. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/__init__.py +0 -0
  41. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/__main__.py +0 -0
  42. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/chat_server.py +0 -0
  43. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/tests/test_browser_chatroom.py +0 -0
  44. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/tests/test_chat_server.py +0 -0
  45. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/chatroom/visual_test.py +0 -0
  46. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/counter.py +0 -0
  47. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/README.md +0 -0
  48. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/__init__.py +0 -0
  49. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/__main__.py +0 -0
  50. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/kanban.py +0 -0
  51. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/tests/renders/kanban-board.png +0 -0
  52. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/tests/test_browser_kanban.py +0 -0
  53. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/tests/test_kanban.py +0 -0
  54. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/kanban/visual_test.py +0 -0
  55. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/README.md +0 -0
  56. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/__init__.py +0 -0
  57. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/__main__.py +0 -0
  58. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/counter.py +0 -0
  59. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/tests/test_android_counter.py +0 -0
  60. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/nativescript_counter/tests/test_counter.py +0 -0
  61. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/README.md +0 -0
  62. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/__init__.py +0 -0
  63. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/__main__.py +0 -0
  64. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/navigation.py +0 -0
  65. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/tests/test_browser_navigation.py +0 -0
  66. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/navigation/tests/test_navigation.py +0 -0
  67. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/dependency_links.txt +0 -0
  68. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/entry_points.txt +0 -0
  69. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/pyjs_examples.egg-info/top_level.txt +0 -0
  70. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/__init__.py +0 -0
  71. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/README.md +0 -0
  72. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/__init__.py +0 -0
  73. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/__main__.py +0 -0
  74. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/state_counter.py +0 -0
  75. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/tests/test_browser_state_counter.py +0 -0
  76. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/state_counter/tests/test_state_counter.py +0 -0
  77. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/README.md +0 -0
  78. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/__init__.py +0 -0
  79. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/__main__.py +0 -0
  80. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/tests/test_browser_ui_dialog.py +0 -0
  81. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/tests/test_ui_dialog.py +0 -0
  82. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/samples/ui_dialog/ui_dialog.py +0 -0
  83. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/setup.cfg +0 -0
  84. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/README.md +0 -0
  85. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/__init__.py +0 -0
  86. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/__main__.py +0 -0
  87. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/tests/renders/action_scene.png +0 -0
  88. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/tests/renders/initial_wave.png +0 -0
  89. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/tests/test_browser_game.py +0 -0
  90. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/space_invaders/tests/test_game.py +0 -0
  91. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/README.md +0 -0
  92. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/__init__.py +0 -0
  93. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/__main__.py +0 -0
  94. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/base.css +0 -0
  95. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/index.css +0 -0
  96. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/todomvc/tests/test_todomvc.py +0 -0
  97. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/README.md +0 -0
  98. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/__init__.py +0 -0
  99. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/__main__.py +0 -0
  100. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/tests/test_browser_workspace.py +0 -0
  101. {pyjs_examples-0.0.2 → pyjs_examples-0.3.0}/workspace/tests/test_workspace.py +0 -0
  102. {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.2
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.2
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 `pyjs-examples` distribution.
28
- Setuptools maps this directory to the `pyjs_examples` import package, so the examples remain easy to
29
- browse in the repository while installing as an ordinary top-level package.
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` extra:
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
- | Example | Command |
46
- | --- | --- |
47
- | [Calculator](calculator/) | `python -m pyjs_examples.calculator` |
48
- | [Chatroom](chatroom/) | `python -m pyjs_examples.chatroom` |
49
- | [Kanban board](kanban/) | `python -m pyjs_examples.kanban` |
50
- | [Navigation workspace](navigation/) | `python -m pyjs_examples.navigation` |
51
- | [NativeScript Counter](nativescript_counter/) | `python -m pyjs_examples.nativescript_counter` |
52
- | [Space Invaders](space_invaders/) | `python -m pyjs_examples.space_invaders` |
53
- | [TodoMVC](todomvc/) | `python -m pyjs_examples.todomvc` |
54
- | [Component workspace](workspace/) | `python -m pyjs_examples.workspace` |
55
- | [Component catalog](catalog.py) | `python -m pyjs_examples.catalog` |
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` entry-point group.
67
- PyJS infers file and directory templates from normal Python module structure instead of hardcoding
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 documentation excerpts
71
- and `pyjs create PROJECT --template NAME`. The initial catalog includes
72
- `pyjs_examples.samples.state_counter` and `pyjs_examples.samples.ui_dialog`.
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(include=True)
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(include=True)
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(include=True)
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(include=True)
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 TypedDict
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(TypedDict):
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(TypedDict):
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
- @js
30
- class OutgoingChatMessage:
31
- def __init__(self, message_type: str, name: str, room: str, text: str):
32
- self.type = message_type
33
- self.name = name
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(include=True)
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(include=True)
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(include=True)
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(include=True)
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(include=True)
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(include=True)
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(include=True)
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(include=True)
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(include=True)
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(include=True)
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(OutgoingChatMessage("join", self.name, self.room, ""))
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(OutgoingChatMessage("switch_room", self.name, room, ""))
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(OutgoingChatMessage("create_room", self.name, room, ""))
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(OutgoingChatMessage("message", self.name, self.room, text))
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()