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::Projection { key, .. } => key.clone().into_py_any(py)?,
86 Value::Quarq(q) => q.source.clone().into_py_any(py)?,
88 Value::List(items) => {
89 let list = PyList::empty(py);
90 for item in items.iter() {
91 list.append(value_to_py(py, item)?)?;
92 }
93 list.unbind().into()
94 }
95 Value::Record(fields) => {
96 let dict = PyDict::new(py);
97 for (k, val) in fields {
98 dict.set_item(k, value_to_py(py, val)?)?;
99 }
100 dict.unbind().into()
101 }
102 Value::Table(t) => value_to_py(py, &t.rows_value())?,
106 Value::Instant {
107 secs,
108 nanos,
109 offset_min,
110 ..
111 } => {
112 let datetime = py.import("datetime")?;
116 let tz = match offset_min {
117 Some(m) => {
118 let delta = datetime
119 .getattr("timedelta")?
120 .call1((0, i64::from(*m) * 60))?;
121 datetime.getattr("timezone")?.call1((delta,))?
122 }
123 None => datetime.getattr("timezone")?.getattr("utc")?,
124 };
125 datetime
126 .getattr("datetime")?
127 .call_method1("fromtimestamp", (*secs, &tz))?
128 .call_method(
129 "replace",
130 (),
131 Some(&[("microsecond", nanos / 1000)].into_py_dict(py)?),
132 )?
133 .unbind()
134 }
135 Value::Quantity { base, value, .. } if base == "s" => {
137 let (secs, nanos) = quarb::split_seconds_pub(*value);
138 py.import("datetime")?
139 .getattr("timedelta")?
140 .call1((0, secs, nanos / 1000))?
141 .unbind()
142 }
143 Value::Period { .. } => v.to_string().into_py_any(py)?,
147 Value::Quantity {
148 value,
149 base,
150 written,
151 } => Quantity {
152 magnitude: *value,
153 unit: base.clone(),
154 written: written.as_ref().map(|(m, u)| format!("{m} {u}")),
155 }
156 .into_pyobject(py)?
157 .into_any()
158 .unbind(),
159 Value::Node(n) => n.locator.as_ref().into_py_any(py)?,
162 Value::Foreign(f) => match f.as_any().downcast_ref::<PyForeign>() {
165 Some(p) => p.obj.clone_ref(py),
166 None => {
167 return Err(pyo3::exceptions::PyTypeError::new_err(format!(
168 "{} is a library value of another library",
169 f.kind()
170 )));
171 }
172 },
173 })
174}
175
176pub fn py_to_value(obj: &Bound<'_, PyAny>) -> PyResult<Value> {
184 let py = obj.py();
185 if obj.is_none() {
186 return Ok(Value::Null);
187 }
188 if let Ok(b) = obj.cast::<PyBool>() {
189 return Ok(Value::Bool(b.is_true()));
190 }
191 if !foreign::is_plain(obj) {
195 let module = obj
196 .get_type()
197 .getattr("__module__")
198 .ok()
199 .and_then(|m| m.extract::<String>().ok())
200 .unwrap_or_default();
201 if module == "numpy" && !obj.hasattr("shape").unwrap_or(false)
202 || module == "numpy"
203 && obj
204 .getattr("shape")
205 .ok()
206 .is_some_and(|s| s.len().unwrap_or(1) == 0)
207 {
208 if let Ok(item) = obj.call_method0("item") {
209 return py_to_value(&item);
210 }
211 }
212 }
213 if obj.hasattr("_fields").unwrap_or(false) {
216 let mut fields = Vec::new();
217 let mut names: Vec<String> = obj.getattr("_fields")?.extract()?;
218 if let Ok(extra) = obj.getattr("_extra_fields") {
219 names.extend(extra.extract::<Vec<String>>().unwrap_or_default());
220 }
221 for n in names {
222 fields.push((n.clone(), py_to_value(&obj.getattr(n.as_str())?)?));
223 }
224 return Ok(Value::Record(fields.into()));
225 }
226 if obj.cast::<PyInt>().is_ok() {
227 if let Ok(i) = obj.extract::<i64>() {
228 return Ok(Value::Int(i));
229 }
230 return Ok(Value::Float(obj.extract::<f64>()?));
231 }
232 if obj.cast::<PyFloat>().is_ok() {
233 return Ok(Value::Float(obj.extract::<f64>()?));
234 }
235 if let Ok(s) = obj.cast::<PyString>() {
236 return Ok(Value::Str(s.to_string_lossy().into_owned()));
237 }
238 if let Ok(q) = obj.extract::<Quantity>() {
239 let written = q.written.as_ref().and_then(|w| {
240 let (m, u) = w.split_once(' ')?;
241 Some((m.parse::<f64>().ok()?, u.to_string()))
242 });
243 return Ok(Value::Quantity {
244 value: q.magnitude,
245 base: q.unit,
246 written,
247 });
248 }
249 if obj.cast::<PyList>().is_ok() || obj.cast::<PyTuple>().is_ok() {
250 let mut items = Vec::new();
251 for item in obj.try_iter()? {
252 items.push(py_to_value(&item?)?);
253 }
254 return Ok(Value::list(items));
255 }
256 if let Ok(d) = obj.cast::<PyDict>() {
257 let mut fields = Vec::new();
258 for (k, v) in d.iter() {
259 fields.push((k.str()?.to_string_lossy().into_owned(), py_to_value(&v)?));
260 }
261 return Ok(Value::Record(fields.into()));
262 }
263 let datetime = py.import("datetime")?;
264 if obj.is_instance(&datetime.getattr("datetime")?)? {
265 let ts: f64 = obj.call_method0("timestamp")?.extract()?;
266 let secs = ts.floor() as i64;
267 let nanos = ((ts - ts.floor()) * 1e9).round() as u32;
268 let offset_min = match obj.call_method0("utcoffset")? {
269 o if o.is_none() => None,
270 o => Some((o.call_method0("total_seconds")?.extract::<f64>()? / 60.0) as i16),
271 };
272 return Ok(Value::Instant {
273 secs,
274 nanos,
275 offset_min,
276 calendar: Default::default(),
277 });
278 }
279 if obj.is_instance(&datetime.getattr("timedelta")?)? {
280 let total: f64 = obj.call_method0("total_seconds")?.extract()?;
281 return Ok(Value::seconds(
282 total.floor() as i64,
283 ((total - total.floor()) * 1e9).round() as u32,
284 ));
285 }
286 if foreign::is_plain(obj) {
287 if obj.hasattr("__iter__")? && !obj.hasattr("read")? {
288 let mut items = Vec::new();
291 for item in obj.try_iter()? {
292 items.push(py_to_value(&item?)?);
293 }
294 return Ok(Value::list(items));
295 }
296 return Ok(Value::Str(obj.str()?.to_string_lossy().into_owned()));
297 }
298 if let Some(v) = foreign::as_table(obj)? {
302 return Ok(v);
303 }
304 Ok(Value::Foreign(quarb::ForeignValue::new(PyForeign::new(
307 obj.clone().unbind(),
308 ))))
309}
310
311pub mod foreign;
312pub mod glue;
313pub mod reflect;
314pub use foreign::PyForeign;
315pub use glue::{PyGlue, importable};
316pub use reflect::Reflective;