1use std::sync::Arc;
2
3use pyo3::exceptions::PyRuntimeError;
4use pyo3::prelude::*;
5use pyo3::types::{PyAny, PyBytes};
6
7#[pyclass(name = "Session", frozen)]
8struct PySession {
9 inner: Arc<onionlink_core::Session>,
10}
11
12fn map_err(err: onionlink_core::Error) -> PyErr {
13 PyRuntimeError::new_err(err.to_string())
14}
15
16fn map_join_err(err: tokio::task::JoinError) -> PyErr {
17 PyRuntimeError::new_err(format!("async runtime task failed: {err}"))
18}
19
20#[pymethods]
21impl PySession {
22 #[new]
23 #[pyo3(signature = (bootstrap = "128.31.0.39:9131", consensus_file = "", timeout_ms = 30000, verbose = false))]
24 fn new(
25 py: Python<'_>,
26 bootstrap: &str,
27 consensus_file: &str,
28 timeout_ms: i32,
29 verbose: bool,
30 ) -> PyResult<Self> {
31 let inner = py
32 .detach(|| onionlink_core::Session::new(bootstrap, consensus_file, timeout_ms, verbose))
33 .map_err(map_err)?;
34 Ok(Self {
35 inner: Arc::new(inner),
36 })
37 }
38
39 #[staticmethod]
40 #[pyo3(signature = (bootstrap = "128.31.0.39:9131", consensus_file = "", timeout_ms = 30000, verbose = false))]
41 fn create_async<'py>(
42 py: Python<'py>,
43 bootstrap: &str,
44 consensus_file: &str,
45 timeout_ms: i32,
46 verbose: bool,
47 ) -> PyResult<Bound<'py, PyAny>> {
48 let bootstrap = bootstrap.to_owned();
49 let consensus_file = consensus_file.to_owned();
50 pyo3_async_runtimes::tokio::future_into_py(py, async move {
51 let inner = tokio::task::spawn_blocking(move || {
52 onionlink_core::Session::new(&bootstrap, &consensus_file, timeout_ms, verbose)
53 })
54 .await
55 .map_err(map_join_err)?
56 .map_err(map_err)?;
57
58 Python::attach(|py| {
59 Py::new(
60 py,
61 PySession {
62 inner: Arc::new(inner),
63 },
64 )
65 })
66 })
67 }
68
69 #[pyo3(signature = (onion, port, payload = Vec::<u8>::new(), response_limit = 4 * 1024 * 1024))]
70 fn request<'py>(
71 &self,
72 py: Python<'py>,
73 onion: &str,
74 port: u16,
75 payload: Vec<u8>,
76 response_limit: usize,
77 ) -> PyResult<Bound<'py, PyBytes>> {
78 let inner = Arc::clone(&self.inner);
79 let onion = onion.to_owned();
80 let inbound = py
81 .detach(move || inner.request(&onion, port, &payload, response_limit))
82 .map_err(map_err)?;
83 Ok(PyBytes::new(py, &inbound))
84 }
85
86 #[pyo3(signature = (onion, port, payload = Vec::<u8>::new(), response_limit = 4 * 1024 * 1024))]
87 fn request_async<'py>(
88 &self,
89 py: Python<'py>,
90 onion: &str,
91 port: u16,
92 payload: Vec<u8>,
93 response_limit: usize,
94 ) -> PyResult<Bound<'py, PyAny>> {
95 let inner = Arc::clone(&self.inner);
96 let onion = onion.to_owned();
97 pyo3_async_runtimes::tokio::future_into_py(py, async move {
98 let inbound = tokio::task::spawn_blocking(move || {
99 inner.request(&onion, port, &payload, response_limit)
100 })
101 .await
102 .map_err(map_join_err)?
103 .map_err(map_err)?;
104
105 Python::attach(|py| Ok(PyBytes::new(py, &inbound).unbind()))
106 })
107 }
108
109 #[pyo3(signature = (onion, port = 80, path = "/", response_limit = 4 * 1024 * 1024))]
110 fn http_get<'py>(
111 &self,
112 py: Python<'py>,
113 onion: &str,
114 port: u16,
115 path: &str,
116 response_limit: usize,
117 ) -> PyResult<Bound<'py, PyBytes>> {
118 let inner = Arc::clone(&self.inner);
119 let onion = onion.to_owned();
120 let path = path.to_owned();
121 let inbound = py
122 .detach(move || inner.http_get(&onion, port, &path, response_limit))
123 .map_err(map_err)?;
124 Ok(PyBytes::new(py, &inbound))
125 }
126
127 #[pyo3(signature = (onion, port = 80, path = "/", response_limit = 4 * 1024 * 1024))]
128 fn http_get_async<'py>(
129 &self,
130 py: Python<'py>,
131 onion: &str,
132 port: u16,
133 path: &str,
134 response_limit: usize,
135 ) -> PyResult<Bound<'py, PyAny>> {
136 let inner = Arc::clone(&self.inner);
137 let onion = onion.to_owned();
138 let path = path.to_owned();
139 pyo3_async_runtimes::tokio::future_into_py(py, async move {
140 let inbound = tokio::task::spawn_blocking(move || {
141 inner.http_get(&onion, port, &path, response_limit)
142 })
143 .await
144 .map_err(map_join_err)?
145 .map_err(map_err)?;
146
147 Python::attach(|py| Ok(PyBytes::new(py, &inbound).unbind()))
148 })
149 }
150}
151
152#[pymodule(gil_used = false)]
153fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
154 m.add("__doc__", "Native Rust bindings for onionlink")?;
155 m.add_class::<PySession>()?;
156 Ok(())
157}