hydro_cli/
cli.rs

1use std::fmt::Display;
2use std::path::PathBuf;
3
4use anyhow::Context;
5use clap::{Parser, Subcommand};
6use pyo3::exceptions::PyException;
7use pyo3::prelude::*;
8use pyo3::types::PyList;
9
10use crate::{AnyhowError, AnyhowWrapper};
11
12#[derive(Parser, Debug)]
13#[command(name = "Hydro Deploy", author, version, about, long_about = None)]
14struct Cli {
15    #[command(subcommand)]
16    command: Commands,
17}
18
19#[derive(Subcommand, Debug)]
20enum Commands {
21    /// Deploys an application given a Python deployment script.
22    Deploy {
23        /// Path to the deployment script.
24        config: PathBuf,
25        /// Additional arguments to pass to the deployment script.
26        #[arg(last(true))]
27        args: Vec<String>,
28    },
29}
30
31fn async_wrapper_module(py: Python) -> Result<&PyModule, PyErr> {
32    PyModule::from_code(
33        py,
34        include_str!("../hydro/async_wrapper.py"),
35        "wrapper.py",
36        "wrapper",
37    )
38}
39
40#[derive(Debug)]
41struct PyErrWithTraceback {
42    err_display: String,
43    traceback: String,
44}
45
46impl std::error::Error for PyErrWithTraceback {}
47
48impl Display for PyErrWithTraceback {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        writeln!(f, "{}", self.err_display)?;
51        write!(f, "{}", self.traceback)
52    }
53}
54
55fn deploy(config: PathBuf, args: Vec<String>) -> anyhow::Result<()> {
56    Python::with_gil(|py| -> anyhow::Result<()> {
57        let syspath: &PyList = py
58            .import("sys")
59            .and_then(|s| s.getattr("path"))
60            .and_then(|p| Ok(p.downcast::<PyList>()?))?;
61
62        syspath.insert(0, PathBuf::from(".").canonicalize().unwrap())?;
63
64        let filename = config.canonicalize().unwrap();
65        let fun: Py<PyAny> = PyModule::from_code(
66            py,
67            std::fs::read_to_string(config).unwrap().as_str(),
68            filename.to_str().unwrap(),
69            "",
70        )
71        .with_context(|| format!("failed to load deployment script: {}", filename.display()))?
72        .getattr("main")
73        .context("expected deployment script to define a `main` function, but one was not found")?
74        .into();
75
76        let wrapper = async_wrapper_module(py)?;
77        match wrapper.call_method1("run", (fun, args)) {
78            Ok(_) => Ok(()),
79            Err(err) => {
80                let traceback = err
81                    .traceback(py)
82                    .context("traceback was expected but none found")
83                    .and_then(|tb| Ok(tb.format()?))?
84                    .trim()
85                    .to_string();
86
87                if err.is_instance_of::<AnyhowError>(py) {
88                    let args = err
89                        .value(py)
90                        .getattr("args")?
91                        .extract::<Vec<AnyhowWrapper>>()?;
92                    let wrapper = args.first().unwrap();
93                    let underlying = &wrapper.underlying;
94                    let mut underlying = underlying.blocking_write();
95                    Err(underlying.take().unwrap()).context(traceback)
96                } else {
97                    Err(PyErrWithTraceback {
98                        err_display: format!("{}", err),
99                        traceback,
100                    }
101                    .into())
102                }
103            }
104        }
105    })?;
106
107    Ok(())
108}
109
110#[pyfunction]
111fn cli_entrypoint(args: Vec<String>) -> PyResult<()> {
112    match Cli::try_parse_from(args) {
113        Ok(args) => {
114            let res = match args.command {
115                Commands::Deploy { config, args } => deploy(config, args),
116            };
117
118            match res {
119                Ok(_) => Ok(()),
120                Err(err) => {
121                    eprintln!("{:?}", err);
122                    Err(PyErr::new::<PyException, _>(""))
123                }
124            }
125        }
126        Err(err) => {
127            err.print().unwrap();
128            Err(PyErr::new::<PyException, _>(""))
129        }
130    }
131}
132
133#[pymodule]
134pub fn cli(_py: Python, m: &PyModule) -> PyResult<()> {
135    m.add_function(wrap_pyfunction!(cli_entrypoint, m)?)?;
136
137    Ok(())
138}