Skip to content

Commit 25c9f2b

Browse files
committed
chore: py client tests
1 parent dcbe34a commit 25c9f2b

16 files changed

+435
-193
lines changed

clients/python/README.md

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# ActorCore Python Client
2+
3+
_The Python client for ActorCore, the Stateful Serverless Framework_
4+
5+
Use this client to connect to ActorCore services from Python applications.
6+
7+
## Resources
8+
9+
- [Quickstart](https://actorcore.org/introduction)
10+
- [Documentation](https://actorcore.org/clients/python)
11+
- [Examples](https://github.com/rivet-gg/actor-core/tree/main/examples)
12+
13+
## Getting Started
14+
15+
### Step 1: Installation
16+
17+
```bash
18+
pip install python-actor-core-client
19+
```
20+
21+
### Step 2: Connect to Actor
22+
23+
```python
24+
from python_actor_core_client import AsyncClient as ActorClient
25+
import asyncio
26+
27+
async def main():
28+
# Create a client connected to your ActorCore manager
29+
client = ActorClient("http://localhost:6420")
30+
31+
# Connect to a chat room actor
32+
chat_room = await client.get("chat-room", tags=[("room", "general")])
33+
34+
# Listen for new messages
35+
chat_room.on_event("newMessage", lambda msg: print(f"New message: {msg}"))
36+
37+
# Send message to room
38+
await chat_room.action("sendMessage", ["alice", "Hello, World!"])
39+
40+
# When finished
41+
await chat_room.disconnect()
42+
43+
if __name__ == "__main__":
44+
asyncio.run(main())
45+
```
46+
47+
### Features
48+
49+
- Async-first design with `AsyncClient`
50+
- Event subscription support via `on_event`
51+
- Action invocation with JSON-serializable arguments
52+
- Simple event handling with `receive` method
53+
- Clean disconnection handling via `disconnect`
54+
55+
## Community & Support
56+
57+
- Join our [Discord](https://rivet.gg/discord)
58+
- Follow us on [X](https://x.com/rivet_gg)
59+
- Follow us on [Bluesky](https://bsky.app/profile/rivet.gg)
60+
- File bug reports in [GitHub Issues](https://github.com/rivet-gg/actor-core/issues)
61+
- Post questions & ideas in [GitHub Discussions](https://github.com/rivet-gg/actor-core/discussions)
62+
63+
## License
64+
65+
Apache 2.0

clients/python/pyproject.toml

+34-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
1-
[build-system]
2-
requires = ["maturin>=1.8,<2.0"]
3-
build-backend = "maturin"
4-
51
[project]
62
name = "python_actor_core_client"
3+
version = "0.7.7"
4+
authors = [
5+
{ name="Rivet Gaming, LLC", email="[email protected]" },
6+
]
7+
description = "Python client for ActorCore - the Stateful Serverless Framework for building AI agents, realtime apps, and game servers"
8+
readme = "README.md"
9+
license = "Apache-2.0"
710
requires-python = ">=3.8"
811
classifiers = [
912
"Programming Language :: Rust",
1013
"Programming Language :: Python :: Implementation :: CPython",
1114
"Programming Language :: Python :: Implementation :: PyPy",
1215
]
13-
dynamic = ["version"]
16+
dependencies = []
17+
18+
[project.urls]
19+
Homepage = "https://github.com/rivet-gg/actor-core"
20+
Issues = "https://github.com/rivet-gg/actor-core/issues"
21+
22+
[project.optional-dependencies]
23+
tests = [
24+
"asyncio==3.4.3",
25+
"iniconfig==2.1.0",
26+
"packaging==24.2",
27+
"pluggy==1.5.0",
28+
"pytest==8.3.5",
29+
"pytest-aio==1.9.0",
30+
]
31+
32+
33+
[build-system]
34+
requires = ["maturin>=1.8,<2.0"]
35+
build-backend = "maturin"
36+
1437
[tool.maturin]
1538
features = ["pyo3/extension-module"]
39+
40+
[tool.pytest.ini_options]
41+
testpaths = ["tests"]
42+
python_files = ["test_*.py"]
43+
log_cli = true
44+
log_level = "DEBUG"

clients/python/requirements.txt

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
-e .
2+
asyncio==3.4.3
3+
iniconfig==2.1.0
24
maturin==1.8.3
5+
packaging==24.2
6+
pluggy==1.5.0
7+
pytest==8.3.5
8+
pytest-aio==1.9.0
9+
310
# source .venv/bin/activate

clients/python/src/simple/async/client.rs

+4-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use pyo3::prelude::*;
55

66
use crate::util::{try_opts_from_kwds, PyKwdArgs};
77

8-
use super::handle::{ActorHandle, InnerActorData};
8+
use super::handle::ActorHandle;
99

1010
#[pyclass(name = "AsyncSimpleClient")]
1111
pub struct Client {
@@ -48,10 +48,7 @@ impl Client {
4848
let handle = client.get(&name, opts).await;
4949

5050
match handle {
51-
Ok(handle) => Ok(ActorHandle {
52-
handle,
53-
data: InnerActorData::new(),
54-
}),
51+
Ok(handle) => Ok(ActorHandle::new(handle)),
5552
Err(e) => Err(py_runtime_err!(
5653
"Failed to get actor: {}",
5754
e
@@ -70,10 +67,7 @@ impl Client {
7067
let handle = client.get_with_id(&id, opts).await;
7168

7269
match handle {
73-
Ok(handle) => Ok(ActorHandle {
74-
handle,
75-
data: InnerActorData::new(),
76-
}),
70+
Ok(handle) => Ok(ActorHandle::new(handle)),
7771
Err(e) => Err(py_runtime_err!(
7872
"Failed to get actor: {}",
7973
e
@@ -92,10 +86,7 @@ impl Client {
9286
let handle = client.create(&name, opts).await;
9387

9488
match handle {
95-
Ok(handle) => Ok(ActorHandle {
96-
handle,
97-
data: InnerActorData::new(),
98-
}),
89+
Ok(handle) => Ok(ActorHandle::new(handle)),
9990
Err(e) => Err(py_runtime_err!(
10091
"Failed to get actor: {}",
10192
e

clients/python/src/simple/async/handle.rs

+35-47
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,40 @@
1-
use std::sync::Arc;
21
use actor_core_client::{self as actor_core_rs};
32
use futures_util::FutureExt;
43
use pyo3::{prelude::*, types::{PyList, PyString, PyTuple}};
5-
use tokio::sync::{mpsc, Mutex};
4+
use tokio::sync::mpsc;
65

76
use crate::util;
87

8+
const EVENT_BUFFER_SIZE: usize = 100;
9+
910
struct ActorEvent {
1011
name: String,
1112
args: Vec<serde_json::Value>,
1213
}
1314

14-
pub struct InnerActorData {
15-
event_tx: Mutex<Option<mpsc::Sender<ActorEvent>>>,
16-
}
17-
18-
impl InnerActorData {
19-
pub fn new() -> Arc<Self> {
20-
Arc::new(Self {
21-
event_tx: Mutex::new(None),
22-
})
23-
}
15+
#[pyclass]
16+
pub struct ActorHandle {
17+
handle: actor_core_rs::handle::ActorHandle,
18+
event_rx: Option<mpsc::Receiver<ActorEvent>>,
19+
event_tx: mpsc::Sender<ActorEvent>,
2420
}
2521

26-
impl InnerActorData {
27-
pub async fn on_event(
28-
&self,
29-
event_name: String,
30-
args: &Vec<serde_json::Value>
31-
) {
32-
let tx = &self.event_tx.lock().await;
33-
let Some(tx) = tx.as_ref() else {
34-
return;
35-
};
36-
37-
tx.send(ActorEvent {
38-
name: event_name,
39-
args: args.clone(),
40-
}).await.map_err(|e| {
41-
py_runtime_err!(
42-
"Failed to send via inner tx: {}",
43-
e
44-
)
45-
}).ok();
22+
impl ActorHandle {
23+
pub fn new(handle: actor_core_rs::handle::ActorHandle) -> Self {
24+
let (event_tx, event_rx) = mpsc::channel(EVENT_BUFFER_SIZE);
25+
26+
Self {
27+
handle,
28+
event_tx,
29+
event_rx: Some(event_rx),
30+
}
4631
}
4732
}
4833

49-
#[pyclass]
50-
pub struct ActorHandle {
51-
pub handle: actor_core_rs::handle::ActorHandle,
52-
pub data: Arc<InnerActorData>,
53-
}
54-
5534
#[pymethods]
5635
impl ActorHandle {
5736
#[new]
58-
pub fn new() -> PyResult<Self> {
37+
pub fn py_new() -> PyResult<Self> {
5938
Err(py_runtime_err!(
6039
"Actor handle cannot be instantiated directly",
6140
))
@@ -106,17 +85,27 @@ impl ActorHandle {
10685
event_name: &str
10786
) -> PyResult<Bound<'a, PyAny>> {
10887
let event_name = event_name.to_string();
109-
let data = self.data.clone();
11088
let handle = self.handle.clone();
89+
let tx = self.event_tx.clone();
11190

11291
pyo3_async_runtimes::tokio::future_into_py(py, async move {
11392
handle.on_event(&event_name.clone(), move |args| {
11493
let event_name = event_name.clone();
11594
let args = args.clone();
116-
let data = data.clone();
95+
let tx = tx.clone();
11796

11897
tokio::spawn(async move {
119-
data.on_event(event_name, &args).await;
98+
let event = ActorEvent {
99+
name: event_name,
100+
args: args.clone(),
101+
};
102+
// Send this upstream(?)
103+
tx.send(event).await.map_err(|e| {
104+
py_runtime_err!(
105+
"Failed to send via inner tx: {}",
106+
e
107+
)
108+
}).ok();
120109
});
121110
}).await;
122111

@@ -126,17 +115,16 @@ impl ActorHandle {
126115

127116
#[pyo3(signature=(count, timeout=None))]
128117
pub fn receive<'a>(
129-
&self,
118+
&mut self,
130119
py: Python<'a>,
131120
count: u32,
132121
timeout: Option<f64>
133122
) -> PyResult<Bound<'a, PyAny>> {
134-
let (tx, mut rx) = mpsc::channel(count as usize);
123+
let mut rx = self.event_rx.take().ok_or_else(|| {
124+
py_runtime_err!("Two .receive() calls cannot co-exist")
125+
})?;
135126

136-
let data = self.data.clone();
137127
pyo3_async_runtimes::tokio::future_into_py(py, async move {
138-
data.event_tx.lock().await.replace(tx);
139-
140128
let result: Vec<ActorEvent> = {
141129
let mut events: Vec<ActorEvent> = Vec::new();
142130

clients/python/src/simple/sync/client.rs

+4-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use actor_core_client::{self as actor_core_rs, CreateOptions, GetOptions, GetWithIdOptions};
22
use pyo3::prelude::*;
33

4-
use super::handle::{ActorHandle, InnerActorData};
4+
use super::handle::ActorHandle;
55
use crate::util::{try_opts_from_kwds, PyKwdArgs, SYNC_RUNTIME};
66

77
#[pyclass(name = "SimpleClient")]
@@ -43,10 +43,7 @@ impl Client {
4343
let handle = SYNC_RUNTIME.block_on(handle);
4444

4545
match handle {
46-
Ok(handle) => Ok(ActorHandle {
47-
handle,
48-
data: InnerActorData::new(),
49-
}),
46+
Ok(handle) => Ok(ActorHandle::new(handle)),
5047
Err(e) => Err(py_runtime_err!(
5148
"Failed to get actor: {}",
5249
e
@@ -61,10 +58,7 @@ impl Client {
6158
let handle = SYNC_RUNTIME.block_on(handle);
6259

6360
match handle {
64-
Ok(handle) => Ok(ActorHandle {
65-
handle,
66-
data: InnerActorData::new(),
67-
}),
61+
Ok(handle) => Ok(ActorHandle::new(handle)),
6862
Err(e) => Err(py_runtime_err!(
6963
"Failed to get actor: {}",
7064
e
@@ -79,10 +73,7 @@ impl Client {
7973
let handle = SYNC_RUNTIME.block_on(handle);
8074

8175
match handle {
82-
Ok(handle) => Ok(ActorHandle {
83-
handle,
84-
data: InnerActorData::new(),
85-
}),
76+
Ok(handle) => Ok(ActorHandle::new(handle)),
8677
Err(e) => Err(py_runtime_err!(
8778
"Failed to get actor: {}",
8879
e

0 commit comments

Comments
 (0)