use crate::integrations::docker_compose::{
BuildOptions, ComposeOutput, ConfigOptions, DockerCompose as RustDockerCompose, DownOptions,
ExecOptions, LogsOptions, PsOptions, RestartOptions, StopOptions, UpOptions,
};
use pyo3::exceptions::{PyIOError, PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use std::collections::HashMap;
#[pyclass(name = "DockerCompose")]
pub struct PyDockerCompose {
inner: RustDockerCompose,
}
#[pyclass(name = "ComposeResult")]
#[derive(Clone)]
pub struct PyComposeOutput {
#[pyo3(get)]
pub success: bool,
#[pyo3(get)]
pub exit_code: i32,
#[pyo3(get)]
pub stdout: String,
#[pyo3(get)]
pub stderr: String,
}
impl From<ComposeOutput> for PyComposeOutput {
fn from(output: ComposeOutput) -> Self {
Self {
success: output.success,
exit_code: output.exit_code,
stdout: output.stdout,
stderr: output.stderr,
}
}
}
#[pymethods]
impl PyDockerCompose {
#[new]
#[pyo3(signature = (file, project_name=None))]
fn new(file: &str, project_name: Option<&str>) -> PyResult<Self> {
let mut compose = RustDockerCompose::new(file).map_err(|e| {
PyIOError::new_err(format!("Failed to create Docker Compose instance: {}", e))
})?;
if let Some(name) = project_name {
compose = compose.with_project_name(name);
}
Ok(Self { inner: compose })
}
#[staticmethod]
fn is_available() -> bool {
RustDockerCompose::is_available()
}
#[getter]
fn compose_file(&self) -> String {
self.inner.compose_file().display().to_string()
}
#[getter]
fn working_dir(&self) -> String {
self.inner.working_dir().display().to_string()
}
#[getter]
fn project_name(&self) -> Option<String> {
self.inner.project_name().map(|s| s.to_string())
}
#[pyo3(signature = (detach=true, build=false, remove_orphans=false, force_recreate=false, no_recreate=false, services=None))]
fn up(
&self,
detach: bool,
build: bool,
remove_orphans: bool,
force_recreate: bool,
no_recreate: bool,
services: Option<Vec<String>>,
) -> PyResult<PyComposeOutput> {
let options = UpOptions {
detach,
build,
remove_orphans,
force_recreate,
no_recreate,
services: services.unwrap_or_default(),
};
let result = self
.inner
.up(options)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose up failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (volumes=false, remove_orphans=false, timeout=None))]
fn down(
&self,
volumes: bool,
remove_orphans: bool,
timeout: Option<String>,
) -> PyResult<PyComposeOutput> {
let options = DownOptions {
volumes,
remove_orphans,
timeout,
};
let result = self
.inner
.down(options)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose down failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (services=None, timeout=None))]
fn restart(
&self,
services: Option<Vec<String>>,
timeout: Option<String>,
) -> PyResult<PyComposeOutput> {
let options = RestartOptions {
timeout,
services: services.unwrap_or_default(),
};
let result = self.inner.restart(options).map_err(|e| {
PyRuntimeError::new_err(format!("Docker Compose restart failed: {}", e))
})?;
Ok(result.into())
}
#[pyo3(signature = (services=None, follow=false, timestamps=false, tail=None, since=None))]
fn logs(
&self,
services: Option<Vec<String>>,
follow: bool,
timestamps: bool,
tail: Option<String>,
since: Option<String>,
) -> PyResult<PyComposeOutput> {
let options = LogsOptions {
follow,
timestamps,
tail,
since,
services: services.unwrap_or_default(),
};
let result = self
.inner
.logs(options)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose logs failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (all=false, quiet=false, services=false, filter_services=None))]
fn ps(
&self,
all: bool,
quiet: bool,
services: bool,
filter_services: Option<Vec<String>>,
) -> PyResult<PyComposeOutput> {
let options = PsOptions {
all,
quiet,
services,
filter_services: filter_services.unwrap_or_default(),
};
let result = self
.inner
.ps(options)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose ps failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (services=None, no_cache=false, pull=false, parallel=false))]
fn build(
&self,
services: Option<Vec<String>>,
no_cache: bool,
pull: bool,
parallel: bool,
) -> PyResult<PyComposeOutput> {
let options = BuildOptions {
no_cache,
pull,
parallel,
services: services.unwrap_or_default(),
};
let result = self
.inner
.build(options)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose build failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (services=None))]
fn start(&self, services: Option<Vec<String>>) -> PyResult<PyComposeOutput> {
let services = services.unwrap_or_default();
let result = self
.inner
.start(&services)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose start failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (services=None, timeout=None))]
fn stop(
&self,
services: Option<Vec<String>>,
timeout: Option<String>,
) -> PyResult<PyComposeOutput> {
let options = StopOptions {
timeout,
services: services.unwrap_or_default(),
};
let result = self
.inner
.stop(options)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose stop failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (service, command, detach=false, tty=true, user=None, workdir=None, env=None))]
#[allow(clippy::too_many_arguments)]
fn exec(
&self,
service: &str,
command: Vec<String>,
detach: bool,
tty: bool,
user: Option<String>,
workdir: Option<String>,
env: Option<HashMap<String, String>>,
) -> PyResult<PyComposeOutput> {
if command.is_empty() {
return Err(PyValueError::new_err("Command cannot be empty"));
}
let options = ExecOptions {
detach,
tty,
user,
workdir,
env: env.unwrap_or_default(),
};
let result = self
.inner
.exec(service, &command, options)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose exec failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (services=None))]
fn pull(&self, services: Option<Vec<String>>) -> PyResult<PyComposeOutput> {
let services = services.unwrap_or_default();
let result = self
.inner
.pull(&services)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose pull failed: {}", e)))?;
Ok(result.into())
}
#[pyo3(signature = (quiet=false, services=false, volumes=false))]
fn config(&self, quiet: bool, services: bool, volumes: bool) -> PyResult<PyComposeOutput> {
let options = ConfigOptions {
quiet,
services,
volumes,
};
let result = self
.inner
.config(options)
.map_err(|e| PyRuntimeError::new_err(format!("Docker Compose config failed: {}", e)))?;
Ok(result.into())
}
fn __repr__(&self) -> String {
format!(
"DockerCompose(file='{}', project_name={:?})",
self.inner.compose_file().display(),
self.inner.project_name()
)
}
}
#[pyfunction]
#[pyo3(signature = (file, project_name=None))]
pub fn compose(file: &str, project_name: Option<&str>) -> PyResult<PyDockerCompose> {
PyDockerCompose::new(file, project_name)
}
#[pymodule]
pub fn compose_integration(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyDockerCompose>()?;
m.add_class::<PyComposeOutput>()?;
m.add_function(wrap_pyfunction!(compose, m)?)?;
Ok(())
}