You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

lib.rs 13 kB

3 years ago
3 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. #![allow(clippy::borrow_deref_ref)] // clippy warns about code generated by #[pymethods]
  2. use std::env::current_dir;
  3. use std::path::PathBuf;
  4. use std::sync::Arc;
  5. use std::time::Duration;
  6. use arrow::pyarrow::{FromPyArrow, ToPyArrow};
  7. use dora_download::download_file;
  8. use dora_node_api::dora_core::config::NodeId;
  9. use dora_node_api::dora_core::descriptor::source_is_url;
  10. use dora_node_api::merged::{MergeExternalSend, MergedEvent};
  11. use dora_node_api::{DataflowId, DoraNode, EventStream};
  12. use dora_operator_api_python::{pydict_to_metadata, DelayedCleanup, NodeCleanupHandle, PyEvent};
  13. use dora_ros2_bridge_python::Ros2Subscription;
  14. use eyre::Context;
  15. use futures::{Stream, StreamExt};
  16. use pyo3::prelude::*;
  17. use pyo3::types::{PyBytes, PyDict};
  18. use pyo3_special_method_derive::{Dict, Dir, Repr, Str};
  19. /// The custom node API lets you integrate `dora` into your application.
  20. /// It allows you to retrieve input and send output in any fashion you want.
  21. ///
  22. /// Use with:
  23. ///
  24. /// ```python
  25. /// from dora import Node
  26. ///
  27. /// node = Node()
  28. /// ```
  29. ///
  30. /// :type node_id: str, optional
  31. #[pyclass]
  32. #[derive(Dir, Dict, Str, Repr)]
  33. pub struct Node {
  34. events: Events,
  35. node: DelayedCleanup<DoraNode>,
  36. dataflow_id: DataflowId,
  37. node_id: NodeId,
  38. }
  39. #[pymethods]
  40. impl Node {
  41. #[new]
  42. #[pyo3(signature = (node_id=None))]
  43. pub fn new(node_id: Option<String>) -> eyre::Result<Self> {
  44. let (node, events) = if let Some(node_id) = node_id {
  45. DoraNode::init_flexible(NodeId::from(node_id))
  46. .context("Could not setup node from node id. Make sure to have a running dataflow with this dynamic node")?
  47. } else {
  48. DoraNode::init_from_env().context("Could not initiate node from environment variable. For dynamic node, please add a node id in the initialization function.")?
  49. };
  50. let dataflow_id = *node.dataflow_id();
  51. let node_id = node.id().clone();
  52. let node = DelayedCleanup::new(node);
  53. let events = events;
  54. let cleanup_handle = NodeCleanupHandle {
  55. _handles: Arc::new(node.handle()),
  56. };
  57. Ok(Node {
  58. events: Events {
  59. inner: EventsInner::Dora(events),
  60. _cleanup_handle: cleanup_handle,
  61. },
  62. dataflow_id,
  63. node_id,
  64. node,
  65. })
  66. }
  67. /// `.next()` gives you the next input that the node has received.
  68. /// It blocks until the next event becomes available.
  69. /// You can use timeout in seconds to return if no input is available.
  70. /// It will return `None` when all senders has been dropped.
  71. ///
  72. /// ```python
  73. /// event = node.next()
  74. /// ```
  75. ///
  76. /// You can also iterate over the event stream with a loop
  77. ///
  78. /// ```python
  79. /// for event in node:
  80. /// match event["type"]:
  81. /// case "INPUT":
  82. /// match event["id"]:
  83. /// case "image":
  84. /// ```
  85. ///
  86. /// :type timeout: float, optional
  87. /// :rtype: dict
  88. #[pyo3(signature = (timeout=None))]
  89. #[allow(clippy::should_implement_trait)]
  90. pub fn next(&mut self, py: Python, timeout: Option<f32>) -> PyResult<Option<Py<PyDict>>> {
  91. let event = py.allow_threads(|| self.events.recv(timeout.map(Duration::from_secs_f32)));
  92. if let Some(event) = event {
  93. let dict = event
  94. .to_py_dict(py)
  95. .context("Could not convert event into a dict")?;
  96. Ok(Some(dict))
  97. } else {
  98. Ok(None)
  99. }
  100. }
  101. /// `.recv_async()` gives you the next input that the node has received asynchronously.
  102. /// It does not blocks until the next event becomes available.
  103. /// You can use timeout in seconds to return if no input is available.
  104. /// It will return an Error if the timeout is reached.
  105. /// It will return `None` when all senders has been dropped.
  106. ///
  107. /// warning::
  108. /// This feature is experimental as pyo3 async (rust-python FFI) is still in development.
  109. ///
  110. /// ```python
  111. /// event = await node.recv_async()
  112. /// ```
  113. ///
  114. /// You can also iterate over the event stream with a loop
  115. ///
  116. /// :type timeout: float, optional
  117. /// :rtype: dict
  118. #[pyo3(signature = (timeout=None))]
  119. #[allow(clippy::should_implement_trait)]
  120. pub async fn recv_async(&mut self, timeout: Option<f32>) -> PyResult<Option<Py<PyDict>>> {
  121. let event = self
  122. .events
  123. .recv_async_timeout(timeout.map(Duration::from_secs_f32))
  124. .await;
  125. if let Some(event) = event {
  126. // Get python
  127. Python::with_gil(|py| {
  128. let dict = event
  129. .to_py_dict(py)
  130. .context("Could not convert event into a dict")?;
  131. Ok(Some(dict))
  132. })
  133. } else {
  134. Ok(None)
  135. }
  136. }
  137. /// You can iterate over the event stream with a loop
  138. ///
  139. /// ```python
  140. /// for event in node:
  141. /// match event["type"]:
  142. /// case "INPUT":
  143. /// match event["id"]:
  144. /// case "image":
  145. /// ```
  146. ///
  147. /// Default behaviour is to timeout after 2 seconds.
  148. ///
  149. /// :rtype: dict
  150. pub fn __next__(&mut self, py: Python) -> PyResult<Option<Py<PyDict>>> {
  151. self.next(py, None)
  152. }
  153. /// You can iterate over the event stream with a loop
  154. ///
  155. /// ```python
  156. /// for event in node:
  157. /// match event["type"]:
  158. /// case "INPUT":
  159. /// match event["id"]:
  160. /// case "image":
  161. /// ```
  162. ///
  163. /// :rtype: dict
  164. fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
  165. slf
  166. }
  167. /// `send_output` send data from the node.
  168. ///
  169. /// ```python
  170. /// Args:
  171. /// output_id: str,
  172. /// data: pyarrow.Array,
  173. /// metadata: Option[Dict],
  174. /// ```
  175. ///
  176. /// ex:
  177. ///
  178. /// ```python
  179. /// node.send_output("string", b"string", {"open_telemetry_context": "7632e76"})
  180. /// ```
  181. ///
  182. /// :type output_id: str
  183. /// :type data: pyarrow.Array
  184. /// :type metadata: dict, optional
  185. /// :rtype: None
  186. #[pyo3(signature = (output_id, data, metadata=None))]
  187. pub fn send_output(
  188. &mut self,
  189. output_id: String,
  190. data: PyObject,
  191. metadata: Option<Bound<'_, PyDict>>,
  192. py: Python,
  193. ) -> eyre::Result<()> {
  194. let parameters = pydict_to_metadata(metadata)?;
  195. if let Ok(py_bytes) = data.downcast_bound::<PyBytes>(py) {
  196. let data = py_bytes.as_bytes();
  197. self.node
  198. .get_mut()
  199. .send_output_bytes(output_id.into(), parameters, data.len(), data)
  200. .wrap_err("failed to send output")?;
  201. } else if let Ok(arrow_array) = arrow::array::ArrayData::from_pyarrow_bound(data.bind(py)) {
  202. self.node.get_mut().send_output(
  203. output_id.into(),
  204. parameters,
  205. arrow::array::make_array(arrow_array),
  206. )?;
  207. } else {
  208. eyre::bail!("invalid `data` type, must by `PyBytes` or arrow array")
  209. }
  210. Ok(())
  211. }
  212. /// Returns the full dataflow descriptor that this node is part of.
  213. ///
  214. /// This method returns the parsed dataflow YAML file.
  215. ///
  216. /// :rtype: dict
  217. pub fn dataflow_descriptor(&mut self, py: Python) -> eyre::Result<PyObject> {
  218. Ok(
  219. pythonize::pythonize(py, &self.node.get_mut().dataflow_descriptor()?)
  220. .map(|x| x.unbind())?,
  221. )
  222. }
  223. /// Returns the dataflow id.
  224. ///
  225. /// :rtype: str
  226. pub fn dataflow_id(&self) -> String {
  227. self.dataflow_id.to_string()
  228. }
  229. /// Merge an external event stream with dora main loop.
  230. /// This currently only work with ROS2.
  231. ///
  232. /// :type subscription: dora.Ros2Subscription
  233. /// :rtype: None
  234. pub fn merge_external_events(
  235. &mut self,
  236. subscription: &mut Ros2Subscription,
  237. ) -> eyre::Result<()> {
  238. let subscription = subscription.into_stream()?;
  239. let stream = futures::stream::poll_fn(move |cx| {
  240. let s = subscription.as_stream().map(|item| {
  241. match item.context("failed to read ROS2 message") {
  242. Ok((value, _info)) => Python::with_gil(|py| {
  243. value
  244. .to_pyarrow(py)
  245. .context("failed to convert value to pyarrow")
  246. .unwrap_or_else(|err| err_to_pyany(err, py))
  247. }),
  248. Err(err) => Python::with_gil(|py| err_to_pyany(err, py)),
  249. }
  250. });
  251. futures::pin_mut!(s);
  252. s.poll_next_unpin(cx)
  253. });
  254. // take out the event stream and temporarily replace it with a dummy
  255. let events = std::mem::replace(
  256. &mut self.events.inner,
  257. EventsInner::Merged(Box::new(futures::stream::empty())),
  258. );
  259. // update self.events with the merged stream
  260. self.events.inner = EventsInner::Merged(events.merge_external_send(Box::pin(stream)));
  261. Ok(())
  262. }
  263. }
  264. fn err_to_pyany(err: eyre::Report, gil: Python<'_>) -> Py<PyAny> {
  265. PyErr::from(err)
  266. .into_pyobject(gil)
  267. .unwrap_or_else(|infallible| match infallible {})
  268. .into_any()
  269. .unbind()
  270. }
  271. struct Events {
  272. inner: EventsInner,
  273. _cleanup_handle: NodeCleanupHandle,
  274. }
  275. impl Events {
  276. fn recv(&mut self, timeout: Option<Duration>) -> Option<PyEvent> {
  277. let event = match &mut self.inner {
  278. EventsInner::Dora(events) => match timeout {
  279. Some(timeout) => events.recv_timeout(timeout).map(MergedEvent::Dora),
  280. None => events.recv().map(MergedEvent::Dora),
  281. },
  282. EventsInner::Merged(events) => futures::executor::block_on(events.next()),
  283. };
  284. event.map(|event| PyEvent { event })
  285. }
  286. async fn recv_async_timeout(&mut self, timeout: Option<Duration>) -> Option<PyEvent> {
  287. let event = match &mut self.inner {
  288. EventsInner::Dora(events) => match timeout {
  289. Some(timeout) => events
  290. .recv_async_timeout(timeout)
  291. .await
  292. .map(MergedEvent::Dora),
  293. None => events.recv_async().await.map(MergedEvent::Dora),
  294. },
  295. EventsInner::Merged(events) => events.next().await,
  296. };
  297. event.map(|event| PyEvent { event })
  298. }
  299. }
  300. #[allow(clippy::large_enum_variant)]
  301. enum EventsInner {
  302. Dora(EventStream),
  303. Merged(Box<dyn Stream<Item = MergedEvent<PyObject>> + Unpin + Send + Sync>),
  304. }
  305. impl<'a> MergeExternalSend<'a, PyObject> for EventsInner {
  306. type Item = MergedEvent<PyObject>;
  307. fn merge_external_send(
  308. self,
  309. external_events: impl Stream<Item = PyObject> + Unpin + Send + Sync + 'a,
  310. ) -> Box<dyn Stream<Item = Self::Item> + Unpin + Send + Sync + 'a> {
  311. match self {
  312. EventsInner::Dora(events) => events.merge_external_send(external_events),
  313. EventsInner::Merged(events) => {
  314. let merged = events.merge_external_send(external_events);
  315. Box::new(merged.map(|event| match event {
  316. MergedEvent::Dora(e) => MergedEvent::Dora(e),
  317. MergedEvent::External(e) => MergedEvent::External(e.flatten()),
  318. }))
  319. }
  320. }
  321. }
  322. }
  323. impl Node {
  324. pub fn id(&self) -> String {
  325. self.node_id.to_string()
  326. }
  327. }
  328. /// Start a runtime for Operators
  329. ///
  330. /// :rtype: None
  331. #[pyfunction]
  332. pub fn start_runtime() -> eyre::Result<()> {
  333. dora_runtime::main().wrap_err("Dora Runtime raised an error.")
  334. }
  335. pub fn resolve_dataflow(dataflow: String) -> eyre::Result<PathBuf> {
  336. let dataflow = if source_is_url(&dataflow) {
  337. // try to download the shared library
  338. let target_path = current_dir().context("Could not access the current dir")?;
  339. let rt = tokio::runtime::Builder::new_current_thread()
  340. .enable_all()
  341. .build()
  342. .context("tokio runtime failed")?;
  343. rt.block_on(async { download_file(&dataflow, &target_path).await })
  344. .wrap_err("failed to download dataflow yaml file")?
  345. } else {
  346. PathBuf::from(dataflow)
  347. };
  348. Ok(dataflow)
  349. }
  350. /// Run a Dataflow
  351. ///
  352. /// :rtype: None
  353. #[pyfunction]
  354. #[pyo3(signature = (dataflow_path, uv=None))]
  355. pub fn run(dataflow_path: String, uv: Option<bool>) -> eyre::Result<()> {
  356. dora_cli::run_func(dataflow_path, uv.unwrap_or_default())
  357. }
  358. #[pymodule]
  359. fn dora(_py: Python, m: Bound<'_, PyModule>) -> PyResult<()> {
  360. dora_ros2_bridge_python::create_dora_ros2_bridge_module(&m)?;
  361. m.add_function(wrap_pyfunction!(start_runtime, &m)?)?;
  362. m.add_function(wrap_pyfunction!(run, &m)?)?;
  363. m.add_class::<Node>()?;
  364. m.setattr("__version__", env!("CARGO_PKG_VERSION"))?;
  365. m.setattr("__author__", "Dora-rs Authors")?;
  366. Ok(())
  367. }