Skip to content

feat: python client #914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[workspace]
members = [
"clients/rust",
"clients/python",
]
72 changes: 72 additions & 0 deletions clients/python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/target

# Byte-compiled / optimized / DLL files
__pycache__/
.pytest_cache/
*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
.venv/
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
include/
man/
venv/
*.egg-info/
.installed.cfg
*.egg

# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-selfcheck.json

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml

# Translations
*.mo

# Mr Developer
.mr.developer.cfg
.project
.pydevproject

# Rope
.ropeproject

# Django stuff:
*.log
*.pot

.DS_Store

# Sphinx documentation
docs/_build/

# PyCharm
.idea/

# VSCode
.vscode/

# Pyenv
.python-version
17 changes: 17 additions & 0 deletions clients/python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "python_actor_core_client"
version = "0.7.7"
edition = "2021"

[lib]
name = "python_actor_core_client"
crate-type = ["cdylib"]

[dependencies]
actor-core-client = { path = "../rust/", version = "0.7.7" }
futures-util = "0.3.31"
once_cell = "1.21.3"
pyo3 = { version = "0.24.0", features = ["extension-module"] }
pyo3-async-runtimes = { version = "0.24.0", features = ["tokio-runtime"] }
serde_json = "1.0.140"
tokio = "1.44.2"
15 changes: 15 additions & 0 deletions clients/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[build-system]
requires = ["maturin>=1.8,<2.0"]
build-backend = "maturin"

[project]
name = "python_actor_core_client"
requires-python = ">=3.8"
classifiers = [
"Programming Language :: Rust",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
[tool.maturin]
features = ["pyo3/extension-module"]
3 changes: 3 additions & 0 deletions clients/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-e .
maturin==1.8.3
# source .venv/bin/activate
129 changes: 129 additions & 0 deletions clients/python/src/events/async/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use std::sync::Arc;

use actor_core_client::{self as actor_core_rs, CreateOptions, GetOptions, GetWithIdOptions};
use pyo3::prelude::*;

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

use super::handle::ActorHandle;

#[pyclass(name = "AsyncClient")]
pub struct Client {
client: Arc<actor_core_rs::Client>,
}

#[pymethods]
impl Client {
#[new]
#[pyo3(signature=(
endpoint,
transport_kind="websocket",
encoding_kind="json"
))]
fn py_new(
endpoint: &str,
transport_kind: &str,
encoding_kind: &str,
) -> PyResult<Self> {
let transport_kind = try_transport_kind_from_str(&transport_kind)?;
let encoding_kind = try_encoding_kind_from_str(&encoding_kind)?;
let client = actor_core_rs::Client::new(
endpoint.to_string(),
transport_kind,
encoding_kind
);

Ok(Client {
client: Arc::new(client)
})
}

#[pyo3(signature = (name, **kwds))]
fn get<'a>(&self, py: Python<'a>, name: &str, kwds: Option<PyKwdArgs>) -> PyResult<Bound<'a, PyAny>> {
let opts = try_opts_from_kwds::<GetOptions>(kwds)?;
let name = name.to_string();
let client = self.client.clone();

pyo3_async_runtimes::tokio::future_into_py(py, async move {
let handle = client.get(&name, opts).await;

match handle {
Ok(handle) => Ok(ActorHandle {
handle
}),
Err(e) => Err(py_runtime_err!(
"Failed to get actor: {}",
e
))
}
})
}

#[pyo3(signature = (id, **kwds))]
fn get_with_id<'a>(&self, py: Python<'a>, id: &str, kwds: Option<PyKwdArgs>) -> PyResult<Bound<'a, PyAny>> {
let opts = try_opts_from_kwds::<GetWithIdOptions>(kwds)?;
let id = id.to_string();
let client = self.client.clone();

pyo3_async_runtimes::tokio::future_into_py(py, async move {
let handle = client.get_with_id(&id, opts).await;

match handle {
Ok(handle) => Ok(ActorHandle {
handle
}),
Err(e) => Err(py_runtime_err!(
"Failed to get actor: {}",
e
))
}
})
}

#[pyo3(signature = (name, **kwds))]
fn create<'a>(&self, py: Python<'a>, name: &str, kwds: Option<PyKwdArgs>) -> PyResult<Bound<'a, PyAny>> {
let opts = try_opts_from_kwds::<CreateOptions>(kwds)?;
let name = name.to_string();
let client = self.client.clone();

pyo3_async_runtimes::tokio::future_into_py(py, async move {
let handle = client.create(&name, opts).await;

match handle {
Ok(handle) => Ok(ActorHandle {
handle
}),
Err(e) => Err(py_runtime_err!(
"Failed to get actor: {}",
e
))
}
})
}
}

fn try_transport_kind_from_str(
transport_kind: &str
) -> PyResult<actor_core_rs::TransportKind> {
match transport_kind {
"websocket" => Ok(actor_core_rs::TransportKind::WebSocket),
"sse" => Ok(actor_core_rs::TransportKind::Sse),
_ => Err(py_value_err!(
"Invalid transport kind: {}",
transport_kind
)),
}
}

fn try_encoding_kind_from_str(
encoding_kind: &str
) -> PyResult<actor_core_rs::EncodingKind> {
match encoding_kind {
"json" => Ok(actor_core_rs::EncodingKind::Json),
"cbor" => Ok(actor_core_rs::EncodingKind::Cbor),
_ => Err(py_value_err!(
"Invalid encoding kind: {}",
encoding_kind
)),
}
}
95 changes: 95 additions & 0 deletions clients/python/src/events/async/handle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@

use actor_core_client::{self as actor_core_rs};
use pyo3::{prelude::*, types::PyTuple};

use crate::util;

#[pyclass]
pub struct ActorHandle {
pub handle: actor_core_rs::handle::ActorHandle,
}

#[pymethods]
impl ActorHandle {
#[new]
pub fn new() -> PyResult<Self> {
Err(py_runtime_err!(
"Actor handle cannot be instantiated directly",
))
}

pub fn action<'a>(
&self,
py: Python<'a>,
method: &str,
args: Vec<PyObject>
) -> PyResult<Bound<'a, PyAny>> {
let method = method.to_string();
let handle = self.handle.clone();

pyo3_async_runtimes::tokio::future_into_py(py, async move {
let args = Python::with_gil(|py| util::py_to_json_value(py, &args))?;
let result = handle.action(&method, args).await;
let Ok(result) = result else {
return Err(py_runtime_err!(
"Failed to call action: {:?}",
result.err()
));
};
let mut result = Python::with_gil(|py| {
match util::json_to_py_value(py, &vec![result]) {
Ok(value) => Ok(
value.iter()
.map(|x| x.clone().unbind())
.collect::<Vec<PyObject>>()
),
Err(e) => Err(e),
}
})?;
let Some(result) = result.drain(0..1).next() else {
return Err(py_runtime_err!(
"Expected one result, got {}",
result.len()
));
};

Ok(result)
})
}

pub fn on_event<'a>(
&self,
py: Python<'a>,
event_name: &str,
callback: PyObject
) -> PyResult<Bound<'a, PyAny>> {
let event_name = event_name.to_string();
let handle = self.handle.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
handle.on_event(&event_name, move |args| {
if let Err(e) = Python::with_gil(|py| -> PyResult<()> {
let args = util::json_to_py_value(py, args)?;
let args = PyTuple::new(py, args)?;

callback.call(py, args, None)?;

Ok(())
}) {
eprintln!("Failed to call event callback: {}", e);
}
}).await;

Ok(())
})
}

pub fn disconnect<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
let handle = self.handle.clone();

pyo3_async_runtimes::tokio::future_into_py(py, async move {
handle.disconnect().await;

Ok(())
})
}
}
11 changes: 11 additions & 0 deletions clients/python/src/events/async/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use pyo3::prelude::*;

mod handle;
mod client;

pub fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<client::Client>()?;


Ok(())
}
12 changes: 12 additions & 0 deletions clients/python/src/events/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use pyo3::prelude::*;

mod sync;
mod r#async;

pub fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
sync::init_module(m)?;
r#async::init_module(m)?;


Ok(())
}
Loading