Skip to content

Commit e2eda4f

Browse files
committed
feat: chat-room-python example (#931)
1 parent 261a31d commit e2eda4f

File tree

9 files changed

+267
-0
lines changed

9 files changed

+267
-0
lines changed

examples/chat-room-python/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.actorcore
2+
node_modules
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { actor, setup } from "actor-core";
2+
3+
// state managed by the actor
4+
export interface State {
5+
messages: { username: string; message: string }[];
6+
}
7+
8+
export const chatRoom = actor({
9+
// initialize state
10+
state: { messages: [] } as State,
11+
12+
// define actions
13+
actions: {
14+
// receive an action call from the client
15+
sendMessage: (c, username: string, message: string) => {
16+
// save message to persistent storage
17+
c.state.messages.push({ username, message });
18+
19+
// broadcast message to all clients
20+
c.broadcast("newMessage", username, message);
21+
},
22+
23+
getHistory: (c) => {
24+
return c.state.messages;
25+
},
26+
},
27+
});
28+
29+
// Create and export the app
30+
export const app = setup({
31+
actors: { chatRoom },
32+
});
33+
34+
// Export type for client type checking
35+
export type App = typeof app;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "chat-room-python",
3+
"version": "0.8.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "npx @actor-core/cli@latest dev actors/app.ts",
8+
"check-types": "tsc --noEmit",
9+
"pytest": "pytest tests/test_chat_room.py -v"
10+
},
11+
"devDependencies": {
12+
"@actor-core/cli": "workspace:*",
13+
"@types/node": "^22.13.9",
14+
"actor-core": "workspace:*",
15+
"tsx": "^3.12.7",
16+
"typescript": "^5.5.2"
17+
},
18+
"example": {
19+
"platforms": [
20+
"*"
21+
]
22+
}
23+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
actor-core-client>=0.8.0
2+
prompt_toolkit>=3.0.0
3+
pytest>=7.0.0
4+
pytest-asyncio>=0.21.0
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import asyncio
2+
from actor_core_client import AsyncClient as ActorClient
3+
import prompt_toolkit
4+
from prompt_toolkit.patch_stdout import patch_stdout
5+
from typing import TypedDict, List
6+
7+
async def init_prompt() -> tuple[str, str]:
8+
username = await prompt_toolkit.prompt_async("Username: ")
9+
room = await prompt_toolkit.prompt_async("Room: ")
10+
return username, room
11+
12+
async def main():
13+
# Get username and room
14+
username, room = await init_prompt()
15+
print(f"Joining room '{room}' as '{username}'")
16+
17+
# Create client and connect to chat room
18+
client = ActorClient("http://localhost:6420")
19+
chat_room = await client.get("chatRoom", tags={"room": room}, params={"room": room})
20+
21+
# Get and display history
22+
history = await chat_room.action("getHistory", [])
23+
if history:
24+
print("\nHistory:")
25+
for msg in history:
26+
print(f"[{msg['username']}] {msg['message']}")
27+
28+
# Set up message handler
29+
def on_message(username: str, message: str):
30+
print(f"\n[{username}] {message}")
31+
32+
chat_room.on_event("newMessage", on_message)
33+
34+
# Main message loop
35+
print("\nStart typing messages (press Ctrl+D or send empty message to exit)")
36+
try:
37+
with patch_stdout():
38+
while True:
39+
# NOTE: Using prompt_toolkit to keep messages
40+
# intact, regardless of other threads / tasks.
41+
message = await prompt_toolkit.prompt_async("\nMessage: ")
42+
if not message:
43+
break
44+
await chat_room.action("sendMessage", [username, message])
45+
except EOFError:
46+
pass
47+
finally:
48+
print("\nDisconnecting...")
49+
await chat_room.disconnect()
50+
51+
if __name__ == "__main__":
52+
asyncio.run(main())
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import asyncio
2+
import os
3+
from actor_core_client import AsyncClient as ActorClient
4+
5+
async def main():
6+
# Create client
7+
endpoint = os.getenv("ENDPOINT", "http://localhost:6420")
8+
client = ActorClient(endpoint)
9+
10+
# Connect to chat room
11+
chat_room = await client.get("chatRoom")
12+
13+
# Get existing messages
14+
messages = await chat_room.action("getHistory", [])
15+
print("Messages:", messages)
16+
17+
# Listen for new messages
18+
def on_message(username: str, message: str):
19+
print(f"Message from {username}: {message}")
20+
21+
chat_room.on_event("newMessage", on_message)
22+
23+
# Send message to room
24+
await chat_room.action("sendMessage", ["william", "All the world's a stage."])
25+
26+
# Disconnect from actor when finished
27+
await chat_room.disconnect()
28+
29+
if __name__ == "__main__":
30+
asyncio.run(main())
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import pytest
2+
from actor_core_client import AsyncClient as ActorClient
3+
from actor_core_test import setup_test
4+
from typing import TypedDict, List
5+
6+
7+
async def test_chat_room_should_handle_messages():
8+
# Set up test environment
9+
client = await setup_test()
10+
11+
# Connect to chat room
12+
chat_room = await client.get("chatRoom")
13+
14+
# Initial history should be empty
15+
initial_messages = await chat_room.action("getHistory", [])
16+
assert initial_messages == []
17+
18+
# Test event emission
19+
received_data = {"username": "", "message": ""}
20+
21+
def on_message(username: str, message: str):
22+
received_data["username"] = username
23+
received_data["message"] = message
24+
25+
chat_room.on_event("newMessage", on_message)
26+
27+
# Send a message
28+
test_user = "william"
29+
test_message = "All the world's a stage."
30+
await chat_room.action("sendMessage", [test_user, test_message])
31+
32+
# Verify event was emitted with correct data
33+
assert received_data["username"] == test_user
34+
assert received_data["message"] == test_message
35+
36+
# Verify message was stored in history
37+
updated_messages = await chat_room.action("getHistory", [])
38+
assert updated_messages == [{"username": test_user, "message": test_message}]
39+
40+
# Send multiple messages and verify
41+
users = ["romeo", "juliet", "othello"]
42+
messages = [
43+
"Wherefore art thou?",
44+
"Here I am!",
45+
"The green-eyed monster."
46+
]
47+
48+
for i in range(len(users)):
49+
await chat_room.action("sendMessage", [users[i], messages[i]])
50+
51+
# Verify event emission
52+
assert received_data["username"] == users[i]
53+
assert received_data["message"] == messages[i]
54+
55+
# Verify all messages are in history in correct order
56+
final_history = await chat_room.action("getHistory", [])
57+
expected_history = [{"username": test_user, "message": test_message}]
58+
expected_history.extend([
59+
{"username": users[i], "message": messages[i]}
60+
for i in range(len(users))
61+
])
62+
63+
assert final_history == expected_history
64+
65+
# Cleanup
66+
await chat_room.disconnect()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"compilerOptions": {
3+
/* Visit https://aka.ms/tsconfig.json to read more about this file */
4+
5+
/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
6+
"target": "esnext",
7+
/* Specify a set of bundled library declaration files that describe the target runtime environment. */
8+
"lib": ["esnext"],
9+
/* Specify what JSX code is generated. */
10+
"jsx": "react-jsx",
11+
12+
/* Specify what module code is generated. */
13+
"module": "esnext",
14+
/* Specify how TypeScript looks up a file from a given module specifier. */
15+
"moduleResolution": "bundler",
16+
/* Specify type package names to be included without being referenced in a source file. */
17+
"types": ["node"],
18+
/* Enable importing .json files */
19+
"resolveJsonModule": true,
20+
21+
/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
22+
"allowJs": true,
23+
/* Enable error reporting in type-checked JavaScript files. */
24+
"checkJs": false,
25+
26+
/* Disable emitting files from a compilation. */
27+
"noEmit": true,
28+
29+
/* Ensure that each file can be safely transpiled without relying on other imports. */
30+
"isolatedModules": true,
31+
/* Allow 'import x from y' when a module doesn't have a default export. */
32+
"allowSyntheticDefaultImports": true,
33+
/* Ensure that casing is correct in imports. */
34+
"forceConsistentCasingInFileNames": true,
35+
36+
/* Enable all strict type-checking options. */
37+
"strict": true,
38+
39+
/* Skip type checking all .d.ts files. */
40+
"skipLibCheck": true
41+
},
42+
"include": ["src/**/*", "actors/**/*", "tests/**/*"]
43+
}

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3897,6 +3897,18 @@ __metadata:
38973897
languageName: node
38983898
linkType: hard
38993899

3900+
"chat-room-python@workspace:examples/chat-room-python":
3901+
version: 0.0.0-use.local
3902+
resolution: "chat-room-python@workspace:examples/chat-room-python"
3903+
dependencies:
3904+
"@actor-core/cli": "workspace:*"
3905+
"@types/node": "npm:^22.13.9"
3906+
actor-core: "workspace:*"
3907+
tsx: "npm:^3.12.7"
3908+
typescript: "npm:^5.5.2"
3909+
languageName: unknown
3910+
linkType: soft
3911+
39003912
"chat-room@workspace:examples/chat-room":
39013913
version: 0.0.0-use.local
39023914
resolution: "chat-room@workspace:examples/chat-room"

0 commit comments

Comments
 (0)