nautilus_core/python/params.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Python bindings for [`Params`] type conversion.
17
18use pyo3::{
19 conversion::IntoPyObjectExt,
20 prelude::*,
21 types::{PyDict, PyList, PyModule},
22};
23use serde_json::Value;
24
25use crate::{
26 params::Params,
27 python::{serialization::from_pyobject_pyo3, to_pyvalue_err},
28};
29
30/// Converts a Python dict to `Params` (IndexMap<String, Value>).
31///
32/// # Errors
33///
34/// Returns a `PyErr` if:
35/// - the dict cannot be serialized to JSON
36/// - the JSON is not a valid object
37pub fn pydict_to_params(py: Python<'_>, dict: &Py<PyDict>) -> PyResult<Option<Params>> {
38 let dict_bound = dict.bind(py);
39 if dict_bound.is_empty() {
40 return Ok(None);
41 }
42
43 from_pyobject_pyo3(py, dict_bound.as_any()).map(Some)
44}
45
46/// Helper function to convert a `serde_json::Value` to a Python object.
47///
48/// This is a common conversion pattern used when converting `Params` to Python dicts.
49///
50/// # Errors
51///
52/// Returns a `PyErr` if the value type is unsupported or conversion fails.
53///
54/// # Panics
55///
56/// Panics if a numeric value claims to be `i64`/`u64`/`f64` via the predicate
57/// methods but the corresponding accessor returns `None` (should not happen for
58/// well-formed [`serde_json::Number`] values).
59pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult<Py<PyAny>> {
60 match val {
61 Value::Null => Ok(py.None()),
62 Value::Bool(b) => b.into_py_any(py),
63 Value::String(s) => s.into_py_any(py),
64 Value::Number(n) => {
65 if n.is_i64() {
66 n.as_i64().unwrap().into_py_any(py)
67 } else if n.is_u64() {
68 n.as_u64().unwrap().into_py_any(py)
69 } else if n.is_f64() {
70 n.as_f64().unwrap().into_py_any(py)
71 } else {
72 Err(to_pyvalue_err("Unsupported JSON number type"))
73 }
74 }
75 Value::Array(arr) => {
76 let py_list = PyList::new(py, &[] as &[Py<PyAny>])?;
77 for item in arr {
78 let py_item = value_to_pyobject(py, item)?;
79 py_list.append(py_item)?;
80 }
81 py_list.into_py_any(py)
82 }
83 Value::Object(_) => {
84 // For nested objects, convert to dict recursively
85 let json_str = serde_json::to_string(val).map_err(to_pyvalue_err)?;
86 let py_dict: Py<PyDict> = PyModule::import(py, "json")?
87 .call_method("loads", (json_str,), None)?
88 .extract()?;
89 py_dict.into_py_any(py)
90 }
91 }
92}
93
94/// Converts `Params` (IndexMap<String, Value>) to a Python dict.
95///
96/// # Errors
97///
98/// Returns a `PyErr` if conversion of any value fails.
99pub fn params_to_pydict(py: Python<'_>, params: &Params) -> PyResult<Py<PyDict>> {
100 let dict = PyDict::new(py);
101 for (key, value) in params {
102 let py_value = value_to_pyobject(py, value)?;
103 dict.set_item(key, py_value)?;
104 }
105 Ok(dict.into())
106}