use pyo3::IntoPyObjectExt;
use pyo3::prelude::*;
use pyo3::types::{IntoPyDict, PyBool, PyDict, PyFloat, PyInt, PyList, PyString, PyTuple};
use quarb::Value;
#[pyclass(frozen, module = "quarb", from_py_object)]
#[derive(Clone, PartialEq)]
pub struct Quantity {
#[pyo3(get)]
pub magnitude: f64,
#[pyo3(get)]
pub unit: String,
#[pyo3(get)]
pub written: Option<String>,
}
#[pymethods]
impl Quantity {
#[new]
#[pyo3(signature = (magnitude, unit, written=None))]
fn new(magnitude: f64, unit: String, written: Option<String>) -> Quantity {
Quantity {
magnitude,
unit,
written,
}
}
fn __repr__(&self) -> String {
match &self.written {
Some(w) => format!(
"Quantity(magnitude={}, unit={:?}, written={:?})",
self.magnitude, self.unit, w
),
None => format!(
"Quantity(magnitude={}, unit={:?})",
self.magnitude, self.unit
),
}
}
fn __str__(&self) -> String {
match &self.written {
Some(w) => w.clone(),
None => format!("{} {}", self.magnitude, self.unit),
}
}
fn __eq__(&self, other: &Quantity) -> bool {
self == other
}
}
pub fn value_to_py(py: Python<'_>, v: &Value) -> PyResult<Py<PyAny>> {
Ok(match v {
Value::Null => py.None(),
Value::Bool(b) => b.into_py_any(py)?,
Value::Int(i) => i.into_py_any(py)?,
Value::Float(f) => f.into_py_any(py)?,
Value::Str(s) => s.into_py_any(py)?,
Value::Projection { key, .. } => key.clone().into_py_any(py)?,
Value::Quarq(q) => q.source.clone().into_py_any(py)?,
Value::List(items) => {
let list = PyList::empty(py);
for item in items.iter() {
list.append(value_to_py(py, item)?)?;
}
list.unbind().into()
}
Value::Record(fields) => {
let dict = PyDict::new(py);
for (k, val) in fields {
dict.set_item(k, value_to_py(py, val)?)?;
}
dict.unbind().into()
}
Value::Table(t) => value_to_py(py, &t.rows_value())?,
Value::Instant {
secs,
nanos,
offset_min,
..
} => {
let datetime = py.import("datetime")?;
let tz = match offset_min {
Some(m) => {
let delta = datetime
.getattr("timedelta")?
.call1((0, i64::from(*m) * 60))?;
datetime.getattr("timezone")?.call1((delta,))?
}
None => datetime.getattr("timezone")?.getattr("utc")?,
};
datetime
.getattr("datetime")?
.call_method1("fromtimestamp", (*secs, &tz))?
.call_method(
"replace",
(),
Some(&[("microsecond", nanos / 1000)].into_py_dict(py)?),
)?
.unbind()
}
Value::Quantity { base, value, .. } if base == "s" => {
let (secs, nanos) = quarb::split_seconds_pub(*value);
py.import("datetime")?
.getattr("timedelta")?
.call1((0, secs, nanos / 1000))?
.unbind()
}
Value::Period { .. } => v.to_string().into_py_any(py)?,
Value::Quantity {
value,
base,
written,
} => Quantity {
magnitude: *value,
unit: base.clone(),
written: written.as_ref().map(|(m, u)| format!("{m} {u}")),
}
.into_pyobject(py)?
.into_any()
.unbind(),
Value::Node(n) => n.locator.as_ref().into_py_any(py)?,
Value::Foreign(f) => match f.as_any().downcast_ref::<PyForeign>() {
Some(p) => p.obj.clone_ref(py),
None => {
return Err(pyo3::exceptions::PyTypeError::new_err(format!(
"{} is a library value of another library",
f.kind()
)));
}
},
})
}
pub fn py_to_value(obj: &Bound<'_, PyAny>) -> PyResult<Value> {
let py = obj.py();
if obj.is_none() {
return Ok(Value::Null);
}
if let Ok(b) = obj.cast::<PyBool>() {
return Ok(Value::Bool(b.is_true()));
}
if !foreign::is_plain(obj) {
let module = obj
.get_type()
.getattr("__module__")
.ok()
.and_then(|m| m.extract::<String>().ok())
.unwrap_or_default();
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)
{
if let Ok(item) = obj.call_method0("item") {
return py_to_value(&item);
}
}
}
if obj.hasattr("_fields").unwrap_or(false) {
let mut fields = Vec::new();
let mut names: Vec<String> = obj.getattr("_fields")?.extract()?;
if let Ok(extra) = obj.getattr("_extra_fields") {
names.extend(extra.extract::<Vec<String>>().unwrap_or_default());
}
for n in names {
fields.push((n.clone(), py_to_value(&obj.getattr(n.as_str())?)?));
}
return Ok(Value::Record(fields.into()));
}
if obj.cast::<PyInt>().is_ok() {
if let Ok(i) = obj.extract::<i64>() {
return Ok(Value::Int(i));
}
return Ok(Value::Float(obj.extract::<f64>()?));
}
if obj.cast::<PyFloat>().is_ok() {
return Ok(Value::Float(obj.extract::<f64>()?));
}
if let Ok(s) = obj.cast::<PyString>() {
return Ok(Value::Str(s.to_string_lossy().into_owned()));
}
if let Ok(q) = obj.extract::<Quantity>() {
let written = q.written.as_ref().and_then(|w| {
let (m, u) = w.split_once(' ')?;
Some((m.parse::<f64>().ok()?, u.to_string()))
});
return Ok(Value::Quantity {
value: q.magnitude,
base: q.unit,
written,
});
}
if obj.cast::<PyList>().is_ok() || obj.cast::<PyTuple>().is_ok() {
let mut items = Vec::new();
for item in obj.try_iter()? {
items.push(py_to_value(&item?)?);
}
return Ok(Value::list(items));
}
if let Ok(d) = obj.cast::<PyDict>() {
let mut fields = Vec::new();
for (k, v) in d.iter() {
fields.push((k.str()?.to_string_lossy().into_owned(), py_to_value(&v)?));
}
return Ok(Value::Record(fields.into()));
}
let datetime = py.import("datetime")?;
if obj.is_instance(&datetime.getattr("datetime")?)? {
let ts: f64 = obj.call_method0("timestamp")?.extract()?;
let secs = ts.floor() as i64;
let nanos = ((ts - ts.floor()) * 1e9).round() as u32;
let offset_min = match obj.call_method0("utcoffset")? {
o if o.is_none() => None,
o => Some((o.call_method0("total_seconds")?.extract::<f64>()? / 60.0) as i16),
};
return Ok(Value::Instant {
secs,
nanos,
offset_min,
calendar: Default::default(),
});
}
if obj.is_instance(&datetime.getattr("timedelta")?)? {
let total: f64 = obj.call_method0("total_seconds")?.extract()?;
return Ok(Value::seconds(
total.floor() as i64,
((total - total.floor()) * 1e9).round() as u32,
));
}
if foreign::is_plain(obj) {
if obj.hasattr("__iter__")? && !obj.hasattr("read")? {
let mut items = Vec::new();
for item in obj.try_iter()? {
items.push(py_to_value(&item?)?);
}
return Ok(Value::list(items));
}
return Ok(Value::Str(obj.str()?.to_string_lossy().into_owned()));
}
if let Some(v) = foreign::as_table(obj)? {
return Ok(v);
}
Ok(Value::Foreign(quarb::ForeignValue::new(PyForeign::new(
obj.clone().unbind(),
))))
}
pub mod foreign;
pub mod glue;
pub mod module;
pub mod reflect;
pub use foreign::PyForeign;
pub use glue::{PyGlue, importable};
pub use module::ModuleEnvironment;
pub use reflect::Reflective;