Skip to content
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

remove external deps #4

Merged
merged 1 commit into from
Jan 17, 2024
Merged
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cuatrorpc_rs"
version = "0.2.2"
version = "0.5.0"
edition = "2021"
authors = ["bleach86 <tux@ghostbyjohnmcafee.com>"]
description = "Fast RPC client library for Python in rust."
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ include = [

[project]
name = "cuatrorpc"
dependencies = ["orjson"]
# dependencies = [""]
repository = "https://github.com/bleach86/CuatroRPC"
requires-python = ">=3.8"
classifiers = [
Expand Down
54 changes: 19 additions & 35 deletions python/cuatrorpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from asyncio import AbstractEventLoop
from concurrent.futures import ThreadPoolExecutor
from cuatrorpc import cuatrorpc_rs
import orjson


class _RpcClientBase:
Expand Down Expand Up @@ -62,7 +61,7 @@ def _callrpc(
self,
method: str,
params: Optional[List[Any]] = None,
wallet: Optional[str] = None,
wallet: str = "",
) -> Any:
"""Private method for making RPC calls
Arguments:
Expand All @@ -73,26 +72,19 @@ def _callrpc(
wallet -- Optional[str]: Optionally specify the wallet to make the call with.
Returns -- Any: Returns the response from the RPC server.
"""
if wallet:
url: str = f"{self.url}/wallet/{wallet}"
else:
url = self.url

if params is None:
params_str: str = "[]"
else:
params_str = orjson.dumps(params).decode()
response: List[int] = cuatrorpc_rs.callrpc_rs(
url,
response: Dict[str, Any] = cuatrorpc_rs.callrpc_rs(
self.url,
method,
params_str,
wallet,
params if params is not None else [],
)
resp_decoded: Dict[str, Any] = orjson.loads(bytes(response))

if resp_decoded["error"]:
raise ValueError("RPC error " + str(resp_decoded["error"]))

return resp_decoded["result"]
if response["error"]:
raise ValueError("RPC error " + str(response["error"]))

return response["result"]


class RpcClient(_RpcClientBase):
Expand All @@ -110,7 +102,7 @@ def callrpc(
self,
method: str,
params: Optional[List[Any]] = None,
wallet: Optional[str] = None,
wallet: str = "",
) -> Any:
"""Method for making RPC calls
Arguments:
Expand Down Expand Up @@ -170,7 +162,7 @@ async def callrpc(
self,
method: str,
params: Optional[List[Any]] = None,
wallet: Optional[str] = None,
wallet: str = "",
) -> Any:
"""Async wrapper method for making RPC calls
Arguments:
Expand Down Expand Up @@ -200,7 +192,7 @@ def _callrpc_cli(
self,
method: str,
params: Optional[List[Any]] = None,
wallet: Optional[str] = None,
wallet: str = "",
) -> Any:
"""Private method for making RPC calls via CLI binary
Arguments:
Expand All @@ -211,28 +203,20 @@ def _callrpc_cli(
wallet -- Optional[str]: Optionally specify the wallet to make the call with.
Returns -- Any: Returns the response from the RPC server.
"""
if wallet is None:
wallet = ""

if params is None:
params_str: str = "[]"
else:
params_str = orjson.dumps(params).decode()

response: str = cuatrorpc_rs.callrpc_cli_rs(
response: Dict[str, Any] = cuatrorpc_rs.callrpc_cli_rs(
self.cli_bin,
self.data_dir,
self.daemon_conf,
method,
wallet,
params_str,
params if params is not None else [],
)
resp_decoded: Dict[str, Any] = orjson.loads(response)

if resp_decoded["error"]:
raise ValueError("RPC error " + str(resp_decoded["error"]))
if response["error"]:
raise ValueError("RPC error " + str(response["error"]))

return resp_decoded["result"]
return response["result"]


class RpcClientCLI(_RpcClientCLIBase):
Expand Down Expand Up @@ -266,7 +250,7 @@ def callrpc_cli(
self,
method: str,
params: Optional[List[Any]] = None,
wallet: Optional[str] = None,
wallet: str = "",
) -> Any:
"""Method for making RPC calls via CLI binary
Arguments:
Expand Down Expand Up @@ -325,7 +309,7 @@ async def callrpc_cli(
self,
method: str,
params: Optional[List[Any]] = None,
wallet: Optional[str] = None,
wallet: str = "",
) -> Any:
"""Async wrapper method for making RPC calls via CLI binary
Arguments:
Expand Down
8 changes: 4 additions & 4 deletions python/cuatrorpc/cuatrorpc_rs.pyi
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import List
from typing import List, Dict, Any

def callrpc_rs(url: str, method: str, params: str) -> List[int]: ...
def callrpc_rs(url: str, method: str, wallet:str, params: List[Any]) -> Dict[str, Any]: ...
def callrpc_cli_rs(
cli_bin: str,
data_dir: str,
daemon_conf: str,
method: str,
wallet: str,
call_args: str,
) -> str: ...
call_args: List[Any],
) -> Dict[str, Any]: ...
151 changes: 119 additions & 32 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyDict, PyList};
use serde_json::Value;
use std::io::Read;
use std::process::Command;

#[pyfunction]
fn callrpc_rs(url: &str, method: &str, params: &str) -> PyResult<Option<Vec<u8>>> {
fn callrpc_rs(
py: Python<'_>,
url: &str,
method: &str,
wallet: &str,
params: &PyList,
) -> PyResult<Option<PyObject>> {
let params_str: String = params.to_string();

let params_sanatized: String = params_str
.replace("'", "\"")
.replace("True", "true")
.replace("False", "false")
.replace("None", "null");

let request_body: String = format!(
r#"
{{
Expand All @@ -14,41 +29,59 @@ fn callrpc_rs(url: &str, method: &str, params: &str) -> PyResult<Option<Vec<u8>>
"params": {}
}}
"#,
method, params
method, params_sanatized
);

let response: ureq::Response = ureq::post(url)
let req_url: String = if wallet.is_empty() {
url.to_string()
} else {
format!("{}/wallet/{}", url, wallet)
};

let response: ureq::Response = ureq::post(&req_url)
.set("Content-Type", "application/json")
.set("User-Agent", "CuatroRPC")
.send_string(&request_body)
.map_err(|e: ureq::Error| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("RPC ERROR: {}", e))
})?;

let len_content: usize = response.header("Content-Length").unwrap().parse()?;

let mut response_bytes: Vec<u8> = Vec::with_capacity(len_content);
response
.into_reader()
.take((len_content + 1) as u64)
.read_to_end(&mut response_bytes)
.map_err(|e: std::io::Error| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Read Error: {}", e))
})?;

Ok(Some(response_bytes))
.map_err(|e: ureq::Error| PyErr::new::<PyRuntimeError, _>(format!("RPC ERROR: {}", e)))?;

let resp_json: Value = response.into_json().map_err(|e: std::io::Error| {
PyErr::new::<PyRuntimeError, _>(format!("JSON Parsing Error: {}", e))
})?;

let result_value: Value = resp_json["result"].clone();
let error_value: i64 = resp_json["error"].as_i64().unwrap_or(0);

let py_dict: &PyDict = PyDict::new(py);

if let Some(py_result) = _response_to_py_object(py, &result_value) {
py_dict.set_item("result", py_result)?;
} else {
py_dict.set_item("result", result_value.to_string())?;
}

py_dict.set_item("error", error_value)?;
Ok(Some(py_dict.into_py(py)))
}

#[pyfunction]
fn callrpc_cli_rs(
py: Python<'_>,
cli_bin: &str,
data_dir: &str,
daemon_conf: &str,
method: &str,
wallet: &str,
call_args: &str,
) -> PyResult<String> {
let parsed_json: Value = serde_json::from_str(call_args).expect("Failed to parse JSON");
call_args: &PyList,
) -> PyResult<PyObject> {
let call_args_str: String = call_args.to_string();

let args_sanatized: String = call_args_str
.replace("'", "\"")
.replace("True", "true")
.replace("False", "false")
.replace("None", "null");

let parsed_json: Value = serde_json::from_str(&args_sanatized).expect("Failed to parse JSON");

let formatted_args: Vec<String> = parsed_json
.as_array()
Expand All @@ -59,7 +92,7 @@ fn callrpc_cli_rs(
if _is_numeric(string_value) || _is_probably_json(string_value) {
string_value.to_string()
} else {
let unquoted_string: &str = _unquote_sting(string_value);
let unquoted_string: &str = _unquote_string(string_value);
unquoted_string.to_string()
}
} else {
Expand All @@ -85,24 +118,78 @@ fn callrpc_cli_rs(
if result.status.success() {
let result_str: std::borrow::Cow<'_, str> = String::from_utf8_lossy(&result.stdout);
let res: String = result_str.replace("\n", "");
if _is_numeric(res.clone()) || _is_probably_json(res.clone()) {
Ok(format!("{{\"result\": {}, \"error\": 0}}", res))
let dict: &PyDict = PyDict::new(py);
if _is_numeric(res.as_str()) {
if !res.contains(".") {
let py_long: i64 = res.parse()?;
dict.set_item("result", py_long)?;
} else {
let py_float: f64 = res.parse()?;
dict.set_item("result", py_float)?;
}
} else {
Ok(format!("{{\"result\": \"{}\", \"error\": 0}}", res))
if let Ok(json_value) = serde_json::from_str::<Value>(&res) {
dict.set_item("result", _response_to_py_object(py, &json_value))?;
} else {
dict.set_item("result", res)?;
}
}
dict.set_item("error", 0)?;
Ok(dict.into())
} else {
let error_message: std::borrow::Cow<'_, str> =
String::from_utf8_lossy(&result.stderr);
let err_res: String = error_message.replace("\n", "");
Ok(format!("{{\"result\": 0, \"error\": \"{}\"}}", err_res))
let dict: &PyDict = PyDict::new(py);
dict.set_item("result", 0)?;
dict.set_item("error", err_res)?;
Ok(dict.into())
}
}
Err(e) => {
let dict: &PyDict = PyDict::new(py);
dict.set_item("result", 0)?;
dict.set_item("error", e.to_string())?;
Ok(dict.into())
}
}
}

fn _response_to_py_object(py: Python<'_>, value: &Value) -> Option<PyObject> {
match value {
Value::Null => None,
Value::Bool(b) => Some(b.clone().into_py(py)),
Value::Number(num) => {
if num.is_i64() {
Some(num.as_i64().unwrap().into_py(py))
} else if num.is_f64() {
Some(num.as_f64().unwrap().into_py(py))
} else {
None
}
}
Value::String(s) => Some(s.clone().into_py(py)),
Value::Array(arr) => {
let py_list: Vec<Py<PyAny>> = arr
.iter()
.map(|v: &Value| _response_to_py_object(py, v))
.collect::<Option<Vec<PyObject>>>()?;
Some(py_list.into_py(py))
}
Value::Object(obj) => {
let py_dict: &PyDict = PyDict::new(py);
for (k, v) in obj {
if let Some(py_value) = _response_to_py_object(py, v) {
py_dict.set_item(k, py_value).ok()?;
}
}
Some(py_dict.into())
}
Err(e) => Ok(format!("{{\"result\": 0, \"error\": \"{}\"}}", e)),
}
}

fn _is_numeric<S: AsRef<str>>(input: S) -> bool {
for char in input.as_ref().chars() {
fn _is_numeric(input: &str) -> bool {
for char in input.chars() {
if !matches!(char, '0'..='9' | '.') {
return false;
}
Expand All @@ -117,7 +204,7 @@ fn _is_probably_json<S: AsRef<str>>(input: S) -> bool {
|| input_str.starts_with("[") && input_str.ends_with("]")
}

fn _unquote_sting<'a, S: AsRef<str> + ?Sized>(input: &'a S) -> &'a str {
fn _unquote_string<'a, S: AsRef<str> + ?Sized>(input: &'a S) -> &'a str {
let input_str: &str = input.as_ref();

let quotes: Vec<&str> = vec!["'", "\"", "'''", "\"\"\""];
Expand Down
Loading