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 indexmap::IndexMap;
19use pyo3::{
20 conversion::IntoPyObjectExt,
21 prelude::*,
22 types::{PyDict, PyList, PyModule},
23};
24use serde_json::Value;
25
26use crate::{params::Params, python::to_pyvalue_err};
27
28/// Converts a Python dict to `Params` (IndexMap<String, Value>).
29///
30/// # Errors
31///
32/// Returns a `PyErr` if:
33/// - the dict cannot be serialized to JSON
34/// - the JSON is not a valid object
35pub fn pydict_to_params(py: Python<'_>, dict: Py<PyDict>) -> PyResult<Option<Params>> {
36 let dict_bound = dict.bind(py);
37 if dict_bound.is_empty() {
38 return Ok(None);
39 }
40
41 let json_str: String = PyModule::import(py, "json")?
42 .call_method("dumps", (dict,), None)?
43 .extract()?;
44 let json_value: Value = serde_json::from_str(&json_str).map_err(to_pyvalue_err)?;
45
46 if let Value::Object(map) = json_value {
47 Ok(Some(Params::from_index_map(
48 map.into_iter().collect::<IndexMap<String, Value>>(),
49 )))
50 } else {
51 Err(to_pyvalue_err("Expected a dictionary"))
52 }
53}
54
55/// Helper function to convert a `serde_json::Value` to a Python object.
56///
57/// This is a common conversion pattern used when converting `Params` to Python dicts.
58///
59/// # Errors
60///
61/// Returns a `PyErr` if the value type is unsupported or conversion fails.
62pub fn value_to_pyobject(py: Python<'_>, val: &Value) -> PyResult<Py<PyAny>> {
63 match val {
64 Value::Null => Ok(py.None()),
65 Value::Bool(b) => b.into_py_any(py),
66 Value::String(s) => s.into_py_any(py),
67 Value::Number(n) => {
68 if n.is_i64() {
69 n.as_i64().unwrap().into_py_any(py)
70 } else if n.is_u64() {
71 n.as_u64().unwrap().into_py_any(py)
72 } else if n.is_f64() {
73 n.as_f64().unwrap().into_py_any(py)
74 } else {
75 Err(to_pyvalue_err("Unsupported JSON number type"))
76 }
77 }
78 Value::Array(arr) => {
79 let py_list =
80 PyList::new(py, &[] as &[Py<PyAny>]).expect("Invalid `ExactSizeIterator`");
81 for item in arr {
82 let py_item = value_to_pyobject(py, item)?;
83 py_list.append(py_item)?;
84 }
85 py_list.into_py_any(py)
86 }
87 Value::Object(_) => {
88 // For nested objects, convert to dict recursively
89 let json_str = serde_json::to_string(val).map_err(to_pyvalue_err)?;
90 let py_dict: Py<PyDict> = PyModule::import(py, "json")?
91 .call_method("loads", (json_str,), None)?
92 .extract()?;
93 py_dict.into_py_any(py)
94 }
95 }
96}
97
98/// Converts `Params` (IndexMap<String, Value>) to a Python dict.
99///
100/// # Errors
101///
102/// Returns a `PyErr` if conversion of any value fails.
103pub fn params_to_pydict(py: Python<'_>, params: &Params) -> PyResult<Py<PyDict>> {
104 let dict = PyDict::new(py);
105 for (key, value) in params {
106 let py_value = value_to_pyobject(py, value)?;
107 dict.set_item(key, py_value)?;
108 }
109 Ok(dict.into())
110}