pyjs-examples 0.0.1__py3-none-any.whl

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 (71) hide show
  1. pyjs_examples/__init__.py +1 -0
  2. pyjs_examples/calculator/README.md +55 -0
  3. pyjs_examples/calculator/__init__.py +1 -0
  4. pyjs_examples/calculator/__main__.py +9 -0
  5. pyjs_examples/calculator/calculator.py +325 -0
  6. pyjs_examples/calculator/tests/test_browser_calculator.py +67 -0
  7. pyjs_examples/calculator/tests/test_calculator.py +72 -0
  8. pyjs_examples/catalog.py +1433 -0
  9. pyjs_examples/chatroom/README.md +50 -0
  10. pyjs_examples/chatroom/__init__.py +1 -0
  11. pyjs_examples/chatroom/__main__.py +12 -0
  12. pyjs_examples/chatroom/chat_server.py +156 -0
  13. pyjs_examples/chatroom/chatroom.py +704 -0
  14. pyjs_examples/chatroom/tests/test_browser_chatroom.py +59 -0
  15. pyjs_examples/chatroom/tests/test_chat_server.py +53 -0
  16. pyjs_examples/chatroom/visual_test.py +156 -0
  17. pyjs_examples/counter.py +73 -0
  18. pyjs_examples/kanban/README.md +31 -0
  19. pyjs_examples/kanban/__init__.py +1 -0
  20. pyjs_examples/kanban/__main__.py +8 -0
  21. pyjs_examples/kanban/kanban.py +581 -0
  22. pyjs_examples/kanban/tests/renders/kanban-board.png +0 -0
  23. pyjs_examples/kanban/tests/test_browser_kanban.py +150 -0
  24. pyjs_examples/kanban/tests/test_kanban.py +38 -0
  25. pyjs_examples/kanban/visual_test.py +108 -0
  26. pyjs_examples/nativescript_counter/README.md +31 -0
  27. pyjs_examples/nativescript_counter/__init__.py +5 -0
  28. pyjs_examples/nativescript_counter/__main__.py +17 -0
  29. pyjs_examples/nativescript_counter/counter.py +54 -0
  30. pyjs_examples/nativescript_counter/tests/test_android_counter.py +31 -0
  31. pyjs_examples/nativescript_counter/tests/test_counter.py +11 -0
  32. pyjs_examples/samples/__init__.py +1 -0
  33. pyjs_examples/samples/state_counter/README.md +19 -0
  34. pyjs_examples/samples/state_counter/__init__.py +1 -0
  35. pyjs_examples/samples/state_counter/__main__.py +3 -0
  36. pyjs_examples/samples/state_counter/state_counter.py +60 -0
  37. pyjs_examples/samples/state_counter/tests/test_browser_state_counter.py +18 -0
  38. pyjs_examples/samples/state_counter/tests/test_state_counter.py +17 -0
  39. pyjs_examples/samples/ui_dialog/README.md +19 -0
  40. pyjs_examples/samples/ui_dialog/__init__.py +1 -0
  41. pyjs_examples/samples/ui_dialog/__main__.py +3 -0
  42. pyjs_examples/samples/ui_dialog/tests/test_browser_ui_dialog.py +21 -0
  43. pyjs_examples/samples/ui_dialog/tests/test_ui_dialog.py +10 -0
  44. pyjs_examples/samples/ui_dialog/ui_dialog.py +65 -0
  45. pyjs_examples/space_invaders/README.md +45 -0
  46. pyjs_examples/space_invaders/__init__.py +1 -0
  47. pyjs_examples/space_invaders/__main__.py +3 -0
  48. pyjs_examples/space_invaders/space_invaders.py +1025 -0
  49. pyjs_examples/space_invaders/tests/renders/action_scene.png +0 -0
  50. pyjs_examples/space_invaders/tests/renders/initial_wave.png +0 -0
  51. pyjs_examples/space_invaders/tests/test_browser_game.py +86 -0
  52. pyjs_examples/space_invaders/tests/test_game.py +83 -0
  53. pyjs_examples/todomvc/README.md +50 -0
  54. pyjs_examples/todomvc/__init__.py +1 -0
  55. pyjs_examples/todomvc/__main__.py +12 -0
  56. pyjs_examples/todomvc/base.css +141 -0
  57. pyjs_examples/todomvc/index.css +393 -0
  58. pyjs_examples/todomvc/tests/test_browser_todomvc.py +145 -0
  59. pyjs_examples/todomvc/tests/test_todomvc.py +84 -0
  60. pyjs_examples/todomvc/todomvc.py +426 -0
  61. pyjs_examples/workspace/README.md +25 -0
  62. pyjs_examples/workspace/__init__.py +1 -0
  63. pyjs_examples/workspace/__main__.py +3 -0
  64. pyjs_examples/workspace/tests/test_browser_workspace.py +109 -0
  65. pyjs_examples/workspace/tests/test_workspace.py +27 -0
  66. pyjs_examples/workspace/workspace.py +749 -0
  67. pyjs_examples-0.0.1.dist-info/METADATA +71 -0
  68. pyjs_examples-0.0.1.dist-info/RECORD +71 -0
  69. pyjs_examples-0.0.1.dist-info/WHEEL +5 -0
  70. pyjs_examples-0.0.1.dist-info/entry_points.txt +11 -0
  71. pyjs_examples-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,50 @@
1
+ # Chatroom Example
2
+
3
+ This example is a small Slack-like workspace built with PyJS custom elements.
4
+ It demonstrates:
5
+
6
+ - HTTP and WebSockets served on the same port
7
+ - a display name stored per browser tab with `sessionStorage`
8
+ - public room creation and room switching
9
+ - real-time messages and room presence
10
+ - DOM-owned scalar state declared with `state()` and rendered with focused effects
11
+ - message and presence collections rendered explicitly from server snapshots
12
+ - entirely in-memory server state that resets on restart
13
+
14
+ ## Run the chatroom
15
+
16
+ From the repository root:
17
+
18
+ ```bash
19
+ .venv/bin/python -m pyjs_examples.chatroom
20
+ ```
21
+
22
+ Open `http://127.0.0.1:8000/` in two tabs, choose a different display name in
23
+ each, and join the same room.
24
+
25
+ ## Run the tests
26
+
27
+ Run the Python server tests:
28
+
29
+ ```bash
30
+ .venv/bin/python -m pytest -q examples/chatroom/tests
31
+ ```
32
+
33
+ Run the custom-element UI tests in the browser:
34
+
35
+ ```bash
36
+ .venv/bin/python -m pytest -q examples/chatroom/tests --pyjs-target=web
37
+ ```
38
+
39
+ ## Run the visual test
40
+
41
+ The visual test starts its own chatroom server, opens two headless browser tabs
42
+ as Alice and Bob, has them create a room and exchange messages, and verifies
43
+ that presence updates when Bob leaves:
44
+
45
+ ```bash
46
+ .venv/bin/python examples/chatroom/visual_test.py
47
+ ```
48
+
49
+ It writes both browser views and a JSON state report to `screenshots/`. The
50
+ chatroom does not need to be running beforehand.
@@ -0,0 +1 @@
1
+ """A multi-room WebSocket chat application with presence."""
@@ -0,0 +1,12 @@
1
+ import os
2
+
3
+ from pyjs.server import serve
4
+
5
+ from .chatroom import render, websocket
6
+
7
+ serve(
8
+ "pyjs_examples.chatroom.chatroom",
9
+ render=render,
10
+ websocket_routes={"/ws": websocket},
11
+ port=int(os.environ.get("PYJS_PORT", "8000")),
12
+ )
@@ -0,0 +1,156 @@
1
+ import json
2
+ from datetime import datetime
3
+
4
+ from websockets.asyncio.server import ServerConnection
5
+ from websockets.exceptions import ConnectionClosed
6
+
7
+ MAX_MESSAGES_PER_ROOM = 200
8
+ MAX_ROOMS = 50
9
+
10
+
11
+ class ChatClient:
12
+ def __init__(self, name: str, room: str):
13
+ self.name = name
14
+ self.room = room
15
+
16
+
17
+ class ChatServer:
18
+ def __init__(self):
19
+ self.rooms: dict[str, list[dict[str, object]]] = {"general": []}
20
+ self.clients: dict[ServerConnection, ChatClient] = {}
21
+ self.next_message_id = 1
22
+
23
+ async def handle(self, connection: ServerConnection):
24
+ try:
25
+ async for raw_message in connection:
26
+ try:
27
+ message = json.loads(raw_message)
28
+ if not isinstance(message, dict):
29
+ raise ValueError("messages must be JSON objects")
30
+ await self.dispatch(connection, message)
31
+ except (json.JSONDecodeError, ValueError) as error:
32
+ await self.send_error(connection, str(error))
33
+ finally:
34
+ if connection in self.clients:
35
+ self.clients.pop(connection)
36
+ await self.broadcast_snapshots()
37
+
38
+ async def dispatch(self, connection: ServerConnection, message: dict[str, object]):
39
+ message_type = message.get("type")
40
+ if message_type == "join":
41
+ await self.join(connection, message)
42
+ return
43
+
44
+ client = self.clients.get(connection)
45
+ if client is None:
46
+ raise ValueError("join the chatroom before sending messages")
47
+ if message_type == "create_room":
48
+ await self.create_room(client, message)
49
+ elif message_type == "switch_room":
50
+ await self.switch_room(client, message)
51
+ elif message_type == "message":
52
+ await self.add_message(client, message)
53
+ else:
54
+ raise ValueError("unknown chat message type")
55
+
56
+ async def join(self, connection: ServerConnection, message: dict[str, object]):
57
+ name = self.valid_name(message.get("name"))
58
+ if any(client.name == name for client in self.clients.values()):
59
+ raise ValueError("that display name is already in use")
60
+ requested_room = self.normalize_room(message.get("room", "general"))
61
+ room = requested_room if requested_room in self.rooms else "general"
62
+ self.clients[connection] = ChatClient(name, room)
63
+ await self.broadcast_snapshots()
64
+
65
+ async def create_room(self, client: ChatClient, message: dict[str, object]):
66
+ room = self.normalize_room(message.get("room"))
67
+ if room not in self.rooms:
68
+ if len(self.rooms) >= MAX_ROOMS:
69
+ raise ValueError("the room limit has been reached")
70
+ self.rooms[room] = []
71
+ client.room = room
72
+ await self.broadcast_snapshots()
73
+
74
+ async def switch_room(self, client: ChatClient, message: dict[str, object]):
75
+ room = self.normalize_room(message.get("room"))
76
+ if room not in self.rooms:
77
+ raise ValueError("that room does not exist")
78
+ client.room = room
79
+ await self.broadcast_snapshots()
80
+
81
+ async def add_message(self, client: ChatClient, message: dict[str, object]):
82
+ text = message.get("text")
83
+ if not isinstance(text, str):
84
+ raise ValueError("message text must be a string")
85
+ text = text.strip()
86
+ if not text:
87
+ return
88
+ if len(text) > 2000:
89
+ raise ValueError("messages cannot exceed 2000 characters")
90
+ room_messages = self.rooms[client.room]
91
+ room_messages.append(
92
+ {
93
+ "id": self.next_message_id,
94
+ "room": client.room,
95
+ "sender": client.name,
96
+ "text": text,
97
+ "time": datetime.now().strftime("%H:%M"),
98
+ }
99
+ )
100
+ self.next_message_id += 1
101
+ if len(room_messages) > MAX_MESSAGES_PER_ROOM:
102
+ del room_messages[:-MAX_MESSAGES_PER_ROOM]
103
+ await self.broadcast_snapshots()
104
+
105
+ def snapshot(self, client: ChatClient):
106
+ users = sorted(
107
+ candidate.name for candidate in self.clients.values() if candidate.room == client.room
108
+ )
109
+ return {
110
+ "type": "snapshot",
111
+ "room": client.room,
112
+ "rooms": list(self.rooms),
113
+ "messages": self.rooms[client.room],
114
+ "users": users,
115
+ }
116
+
117
+ async def broadcast_snapshots(self):
118
+ disconnected = []
119
+ for connection, client in list(self.clients.items()):
120
+ try:
121
+ await connection.send(json.dumps(self.snapshot(client)))
122
+ except ConnectionClosed:
123
+ disconnected.append(connection)
124
+ for connection in disconnected:
125
+ self.clients.pop(connection, None)
126
+
127
+ if disconnected:
128
+ for connection, client in list(self.clients.items()):
129
+ try:
130
+ await connection.send(json.dumps(self.snapshot(client)))
131
+ except ConnectionClosed:
132
+ self.clients.pop(connection, None)
133
+
134
+ async def send_error(self, connection: ServerConnection, message: str):
135
+ await connection.send(json.dumps({"type": "error", "message": message}))
136
+
137
+ def valid_name(self, value: object) -> str:
138
+ if not isinstance(value, str):
139
+ raise ValueError("display name must be a string")
140
+ name = value.strip()
141
+ if not name or len(name) > 32:
142
+ raise ValueError("display name must contain 1 to 32 characters")
143
+ return name
144
+
145
+ def normalize_room(self, value: object) -> str:
146
+ if not isinstance(value, str):
147
+ raise ValueError("room name must be a string")
148
+ room = value.strip().lower().replace(" ", "-")
149
+ if not room or len(room) > 32:
150
+ raise ValueError("room name must contain 1 to 32 characters")
151
+ if not all(character.isalnum() or character in "-_" for character in room):
152
+ raise ValueError("room names may contain letters, numbers, hyphens, and underscores")
153
+ return room
154
+
155
+
156
+ chat_server = ChatServer()