1use pyo3::IntoPyObjectExt;
20use pyo3::prelude::*;
21use pyo3::types::{IntoPyDict, PyBool, PyDict, PyFloat, PyInt, PyList, PyString, PyTuple};
22use quarb::Value;
23
24#[pyclass(frozen, module = "quarb", from_py_object)]
29#[derive(Clone, PartialEq)]
30pub struct Quantity {
31 #[pyo3(get)]
32 pub magnitude: f64,
33 #[pyo3(get)]
34 pub unit: String,
35 #[pyo3(get)]
36 pub written: Option<String>,
37}
38
39#[pymethods]
40impl Quantity {
41 #[new]
42 #[pyo3(signature = (magnitude, unit, written=None))]
43 fn new(magnitude: f64, unit: String, written: Option<String>) -> Quantity {
44 Quantity {
45 magnitude,
46 unit,
47 written,
48 }
49 }
50
51 fn __repr__(&self) -> String {
52 match &self.written {
53 Some(w) => format!(
54 "Quantity(magnitude={}, unit={:?}, written={:?})",
55 self.magnitude, self.unit, w
56 ),
57 None => format!(
58 "Quantity(magnitude={}, unit={:?})",
59 self.magnitude, self.unit
60 ),
61 }
62 }
63
64 fn __str__(&self) -> String {
65 match &self.written {
66 Some(w) => w.clone(),
67 None => format!("{} {}", self.magnitude, self.unit),
68 }
69 }
70
71 fn __eq__(&self, other: &Quantity) -> bool {
72 self == other
73 }
74}
75
76pub fn value_to_py(py: Python<'_>, v: &Value) -> PyResult<Py<PyAny>> {
78 Ok(match v {
79 Value::Null => py.None(),
80 Value::Bool(b) => b.into_py_any(py)?,
81 Value::Int(i) => i.into_py_any(py)?,
82 Value::Float(f) => f.into_py_any(py)?,
83 Value::Str(s) => s.into_py_any(py)?,
84 Value::List(items) => {
85 let list = PyList::empty(py);
86 for item in items.iter() {
87 list.append(value_to_py(py, item)?)?;
88 }
89 list.unbind().into()
90 }
91 Value::Record(fields) => {
92 let dict = PyDict::new(py);
93 for (k, val) in fields {
94 dict.set_item(k, value_to_py(py, val)?)?;
95 }
96 dict.unbind().into()
97 }
98 Value::Table(t) => value_to_py(py, &t.rows_value())?,
102 Value::Instant {
103 secs,
104 nanos,
105 offset_min,
106 } => {
107 let datetime = py.import("datetime")?;
111 let tz = match offset_min {
112 Some(m) => {
113 let delta = datetime
114 .getattr("timedelta")?
115 .call1((0, i64::from(*m) * 60))?;
116 datetime.getattr("timezone")?.call1((delta,))?
117 }
118 None => datetime.getattr("timezone")?.getattr("utc")?,
119 };
120 datetime
121 .getattr("datetime")?
122 .call_method1("fromtimestamp", (*secs, &tz))?
123 .call_method(
124 "replace",
125 (),
126 Some(&[("microsecond", nanos / 1000)].into_py_dict(py)?),
127 )?
128 .unbind()
129 }
130 Value::Duration { secs, nanos } => py
131 .import("datetime")?
132 .getattr("timedelta")?
133 .call1((0, *secs, nanos / 1000))?
134 .unbind(),
135 Value::Period { .. } => v.to_string().into_py_any(py)?,
139 Value::Quantity {
140 value,
141 base,
142 written,
143 } => Quantity {
144 magnitude: *value,
145 unit: base.clone(),
146 written: written.as_ref().map(|(m, u)| format!("{m} {u}")),
147 }
148 .into_pyobject(py)?
149 .into_any()
150 .unbind(),
151 Value::Foreign(f) => match f.as_any().downcast_ref::<PyForeign>() {
154 Some(p) => p.obj.clone_ref(py),
155 None => {
156 return Err(pyo3::exceptions::PyTypeError::new_err(format!(
157 "{} is a library value of another library",
158 f.kind()
159 )));
160 }
161 },
162 })
163}
164
165pub fn py_to_value(obj: &Bound<'_, PyAny>) -> PyResult<Value> {
173 let py = obj.py();
174 if obj.is_none() {
175 return Ok(Value::Null);
176 }
177 if let Ok(b) = obj.cast::<PyBool>() {
178 return Ok(Value::Bool(b.is_true()));
179 }
180 if !foreign::is_plain(obj) {
184 let module = obj
185 .get_type()
186 .getattr("__module__")
187 .ok()
188 .and_then(|m| m.extract::<String>().ok())
189 .unwrap_or_default();
190 if module == "numpy" && !obj.hasattr("shape").unwrap_or(false) || module == "numpy" && obj.getattr("shape").ok().is_some_and(|s| s.len().unwrap_or(1) == 0) {
191 if let Ok(item) = obj.call_method0("item") {
192 return py_to_value(&item);
193 }
194 }
195 }
196 if obj.hasattr("_fields").unwrap_or(false) {
199 let mut fields = Vec::new();
200 let mut names: Vec<String> = obj.getattr("_fields")?.extract()?;
201 if let Ok(extra) = obj.getattr("_extra_fields") {
202 names.extend(extra.extract::<Vec<String>>().unwrap_or_default());
203 }
204 for n in names {
205 fields.push((n.clone(), py_to_value(&obj.getattr(n.as_str())?)?));
206 }
207 return Ok(Value::Record(fields));
208 }
209 if obj.cast::<PyInt>().is_ok() {
210 if let Ok(i) = obj.extract::<i64>() {
211 return Ok(Value::Int(i));
212 }
213 return Ok(Value::Float(obj.extract::<f64>()?));
214 }
215 if obj.cast::<PyFloat>().is_ok() {
216 return Ok(Value::Float(obj.extract::<f64>()?));
217 }
218 if let Ok(s) = obj.cast::<PyString>() {
219 return Ok(Value::Str(s.to_string_lossy().into_owned()));
220 }
221 if let Ok(q) = obj.extract::<Quantity>() {
222 let written = q.written.as_ref().and_then(|w| {
223 let (m, u) = w.split_once(' ')?;
224 Some((m.parse::<f64>().ok()?, u.to_string()))
225 });
226 return Ok(Value::Quantity {
227 value: q.magnitude,
228 base: q.unit,
229 written,
230 });
231 }
232 if obj.cast::<PyList>().is_ok() || obj.cast::<PyTuple>().is_ok() {
233 let mut items = Vec::new();
234 for item in obj.try_iter()? {
235 items.push(py_to_value(&item?)?);
236 }
237 return Ok(Value::list(items));
238 }
239 if let Ok(d) = obj.cast::<PyDict>() {
240 let mut fields = Vec::new();
241 for (k, v) in d.iter() {
242 fields.push((k.str()?.to_string_lossy().into_owned(), py_to_value(&v)?));
243 }
244 return Ok(Value::Record(fields));
245 }
246 let datetime = py.import("datetime")?;
247 if obj.is_instance(&datetime.getattr("datetime")?)? {
248 let ts: f64 = obj.call_method0("timestamp")?.extract()?;
249 let secs = ts.floor() as i64;
250 let nanos = ((ts - ts.floor()) * 1e9).round() as u32;
251 let offset_min = match obj.call_method0("utcoffset")? {
252 o if o.is_none() => None,
253 o => Some((o.call_method0("total_seconds")?.extract::<f64>()? / 60.0) as i16),
254 };
255 return Ok(Value::Instant {
256 secs,
257 nanos,
258 offset_min,
259 });
260 }
261 if obj.is_instance(&datetime.getattr("timedelta")?)? {
262 let total: f64 = obj.call_method0("total_seconds")?.extract()?;
263 return Ok(Value::Duration {
264 secs: total.floor() as i64,
265 nanos: ((total - total.floor()) * 1e9).round() as u32,
266 });
267 }
268 if foreign::is_plain(obj) {
269 if obj.hasattr("__iter__")? && !obj.hasattr("read")? {
270 let mut items = Vec::new();
273 for item in obj.try_iter()? {
274 items.push(py_to_value(&item?)?);
275 }
276 return Ok(Value::list(items));
277 }
278 return Ok(Value::Str(obj.str()?.to_string_lossy().into_owned()));
279 }
280 if let Some(v) = foreign::as_table(obj)? {
284 return Ok(v);
285 }
286 Ok(Value::Foreign(quarb::ForeignValue::new(PyForeign::new(
289 obj.clone().unbind(),
290 ))))
291}
292
293pub mod foreign;
294pub mod glue;
295pub mod reflect;
296pub use foreign::PyForeign;
297pub use glue::{PyGlue, importable};
298pub use reflect::Reflective;