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