Skip to content

Commit 80177d6

Browse files
committed
feat: python client
1 parent 16782e1 commit 80177d6

29 files changed

+1560
-6
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[workspace]
22
members = [
33
"clients/rust",
4+
"clients/python",
45
]

clients/python/.gitignore

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/target
2+
3+
# Byte-compiled / optimized / DLL files
4+
__pycache__/
5+
.pytest_cache/
6+
*.py[cod]
7+
8+
# C extensions
9+
*.so
10+
11+
# Distribution / packaging
12+
.Python
13+
.venv/
14+
env/
15+
bin/
16+
build/
17+
develop-eggs/
18+
dist/
19+
eggs/
20+
lib/
21+
lib64/
22+
parts/
23+
sdist/
24+
var/
25+
include/
26+
man/
27+
venv/
28+
*.egg-info/
29+
.installed.cfg
30+
*.egg
31+
32+
# Installer logs
33+
pip-log.txt
34+
pip-delete-this-directory.txt
35+
pip-selfcheck.json
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.coverage
41+
.cache
42+
nosetests.xml
43+
coverage.xml
44+
45+
# Translations
46+
*.mo
47+
48+
# Mr Developer
49+
.mr.developer.cfg
50+
.project
51+
.pydevproject
52+
53+
# Rope
54+
.ropeproject
55+
56+
# Django stuff:
57+
*.log
58+
*.pot
59+
60+
.DS_Store
61+
62+
# Sphinx documentation
63+
docs/_build/
64+
65+
# PyCharm
66+
.idea/
67+
68+
# VSCode
69+
.vscode/
70+
71+
# Pyenv
72+
.python-version

clients/python/Cargo.toml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "python_actor_core_client"
3+
version = "0.7.7"
4+
edition = "2021"
5+
6+
[lib]
7+
name = "python_actor_core_client"
8+
crate-type = ["cdylib"]
9+
10+
[dependencies]
11+
actor-core-client = { path = "../rust/", version = "0.7.7" }
12+
futures-util = "0.3.31"
13+
once_cell = "1.21.3"
14+
pyo3 = { version = "0.24.0", features = ["extension-module"] }
15+
pyo3-async-runtimes = { version = "0.24.0", features = ["tokio-runtime"] }
16+
serde_json = "1.0.140"
17+
tokio = "1.44.2"

clients/python/pyproject.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[build-system]
2+
requires = ["maturin>=1.8,<2.0"]
3+
build-backend = "maturin"
4+
5+
[project]
6+
name = "python_actor_core_client"
7+
requires-python = ">=3.8"
8+
classifiers = [
9+
"Programming Language :: Rust",
10+
"Programming Language :: Python :: Implementation :: CPython",
11+
"Programming Language :: Python :: Implementation :: PyPy",
12+
]
13+
dynamic = ["version"]
14+
[tool.maturin]
15+
features = ["pyo3/extension-module"]

clients/python/requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-e .
2+
maturin==1.8.3
3+
# source .venv/bin/activate
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use std::sync::Arc;
2+
3+
use actor_core_client::{self as actor_core_rs, CreateOptions, GetOptions, GetWithIdOptions};
4+
use pyo3::prelude::*;
5+
6+
use crate::util::{try_opts_from_kwds, PyKwdArgs};
7+
8+
use super::handle::ActorHandle;
9+
10+
#[pyclass(name = "AsyncClient")]
11+
pub struct Client {
12+
client: Arc<actor_core_rs::Client>,
13+
}
14+
15+
#[pymethods]
16+
impl Client {
17+
#[new]
18+
#[pyo3(signature=(
19+
endpoint,
20+
transport_kind="websocket",
21+
encoding_kind="json"
22+
))]
23+
fn py_new(
24+
endpoint: &str,
25+
transport_kind: &str,
26+
encoding_kind: &str,
27+
) -> PyResult<Self> {
28+
let transport_kind = try_transport_kind_from_str(&transport_kind)?;
29+
let encoding_kind = try_encoding_kind_from_str(&encoding_kind)?;
30+
let client = actor_core_rs::Client::new(
31+
endpoint.to_string(),
32+
transport_kind,
33+
encoding_kind
34+
);
35+
36+
Ok(Client {
37+
client: Arc::new(client)
38+
})
39+
}
40+
41+
#[pyo3(signature = (name, **kwds))]
42+
fn get<'a>(&self, py: Python<'a>, name: &str, kwds: Option<PyKwdArgs>) -> PyResult<Bound<'a, PyAny>> {
43+
let opts = try_opts_from_kwds::<GetOptions>(kwds)?;
44+
let name = name.to_string();
45+
let client = self.client.clone();
46+
47+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
48+
let handle = client.get(&name, opts).await;
49+
50+
match handle {
51+
Ok(handle) => Ok(ActorHandle {
52+
handle
53+
}),
54+
Err(e) => Err(py_runtime_err!(
55+
"Failed to get actor: {}",
56+
e
57+
))
58+
}
59+
})
60+
}
61+
62+
#[pyo3(signature = (id, **kwds))]
63+
fn get_with_id<'a>(&self, py: Python<'a>, id: &str, kwds: Option<PyKwdArgs>) -> PyResult<Bound<'a, PyAny>> {
64+
let opts = try_opts_from_kwds::<GetWithIdOptions>(kwds)?;
65+
let id = id.to_string();
66+
let client = self.client.clone();
67+
68+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
69+
let handle = client.get_with_id(&id, opts).await;
70+
71+
match handle {
72+
Ok(handle) => Ok(ActorHandle {
73+
handle
74+
}),
75+
Err(e) => Err(py_runtime_err!(
76+
"Failed to get actor: {}",
77+
e
78+
))
79+
}
80+
})
81+
}
82+
83+
#[pyo3(signature = (name, **kwds))]
84+
fn create<'a>(&self, py: Python<'a>, name: &str, kwds: Option<PyKwdArgs>) -> PyResult<Bound<'a, PyAny>> {
85+
let opts = try_opts_from_kwds::<CreateOptions>(kwds)?;
86+
let name = name.to_string();
87+
let client = self.client.clone();
88+
89+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
90+
let handle = client.create(&name, opts).await;
91+
92+
match handle {
93+
Ok(handle) => Ok(ActorHandle {
94+
handle
95+
}),
96+
Err(e) => Err(py_runtime_err!(
97+
"Failed to get actor: {}",
98+
e
99+
))
100+
}
101+
})
102+
}
103+
}
104+
105+
fn try_transport_kind_from_str(
106+
transport_kind: &str
107+
) -> PyResult<actor_core_rs::TransportKind> {
108+
match transport_kind {
109+
"websocket" => Ok(actor_core_rs::TransportKind::WebSocket),
110+
"sse" => Ok(actor_core_rs::TransportKind::Sse),
111+
_ => Err(py_value_err!(
112+
"Invalid transport kind: {}",
113+
transport_kind
114+
)),
115+
}
116+
}
117+
118+
fn try_encoding_kind_from_str(
119+
encoding_kind: &str
120+
) -> PyResult<actor_core_rs::EncodingKind> {
121+
match encoding_kind {
122+
"json" => Ok(actor_core_rs::EncodingKind::Json),
123+
"cbor" => Ok(actor_core_rs::EncodingKind::Cbor),
124+
_ => Err(py_value_err!(
125+
"Invalid encoding kind: {}",
126+
encoding_kind
127+
)),
128+
}
129+
}
+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
2+
use actor_core_client::{self as actor_core_rs};
3+
use pyo3::{prelude::*, types::PyTuple};
4+
5+
use crate::util;
6+
7+
#[pyclass]
8+
pub struct ActorHandle {
9+
pub handle: actor_core_rs::handle::ActorHandle,
10+
}
11+
12+
#[pymethods]
13+
impl ActorHandle {
14+
#[new]
15+
pub fn new() -> PyResult<Self> {
16+
Err(py_runtime_err!(
17+
"Actor handle cannot be instantiated directly",
18+
))
19+
}
20+
21+
pub fn action<'a>(
22+
&self,
23+
py: Python<'a>,
24+
method: &str,
25+
args: Vec<PyObject>
26+
) -> PyResult<Bound<'a, PyAny>> {
27+
let method = method.to_string();
28+
let handle = self.handle.clone();
29+
30+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
31+
let args = Python::with_gil(|py| util::py_to_json_value(py, &args))?;
32+
let result = handle.action(&method, args).await;
33+
let Ok(result) = result else {
34+
return Err(py_runtime_err!(
35+
"Failed to call action: {:?}",
36+
result.err()
37+
));
38+
};
39+
let mut result = Python::with_gil(|py| {
40+
match util::json_to_py_value(py, &vec![result]) {
41+
Ok(value) => Ok(
42+
value.iter()
43+
.map(|x| x.clone().unbind())
44+
.collect::<Vec<PyObject>>()
45+
),
46+
Err(e) => Err(e),
47+
}
48+
})?;
49+
let Some(result) = result.drain(0..1).next() else {
50+
return Err(py_runtime_err!(
51+
"Expected one result, got {}",
52+
result.len()
53+
));
54+
};
55+
56+
Ok(result)
57+
})
58+
}
59+
60+
pub fn on_event<'a>(
61+
&self,
62+
py: Python<'a>,
63+
event_name: &str,
64+
callback: PyObject
65+
) -> PyResult<Bound<'a, PyAny>> {
66+
let event_name = event_name.to_string();
67+
let handle = self.handle.clone();
68+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
69+
handle.on_event(&event_name, move |args| {
70+
if let Err(e) = Python::with_gil(|py| -> PyResult<()> {
71+
let args = util::json_to_py_value(py, args)?;
72+
let args = PyTuple::new(py, args)?;
73+
74+
callback.call(py, args, None)?;
75+
76+
Ok(())
77+
}) {
78+
eprintln!("Failed to call event callback: {}", e);
79+
}
80+
}).await;
81+
82+
Ok(())
83+
})
84+
}
85+
86+
pub fn disconnect<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
87+
let handle = self.handle.clone();
88+
89+
pyo3_async_runtimes::tokio::future_into_py(py, async move {
90+
handle.disconnect().await;
91+
92+
Ok(())
93+
})
94+
}
95+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use pyo3::prelude::*;
2+
3+
mod handle;
4+
mod client;
5+
6+
pub fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
7+
m.add_class::<client::Client>()?;
8+
9+
10+
Ok(())
11+
}

clients/python/src/events/mod.rs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use pyo3::prelude::*;
2+
3+
mod sync;
4+
mod r#async;
5+
6+
pub fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
7+
sync::init_module(m)?;
8+
r#async::init_module(m)?;
9+
10+
11+
Ok(())
12+
}

0 commit comments

Comments
 (0)