docker_pyo3/
container.rs

1use chrono::{DateTime, Utc};
2use docker_api::conn::TtyChunk;
3use docker_api::models::{
4    ContainerChanges200Response, ContainerInspect200Response, ContainerPrune200Response,
5    ContainerSummary, ContainerTop200Response, ContainerWaitResponse,
6};
7use docker_api::opts::{
8    ContainerCommitOpts, ContainerCreateOpts, ContainerListOpts, ContainerPruneOpts,
9    ContainerRestartOpts, ContainerStopOpts, ExecCreateOpts, ExecStartOpts, LogsOpts, PublishPort,
10};
11use docker_api::{Container, Containers, Exec};
12use futures_util::stream::StreamExt;
13use futures_util::TryStreamExt;
14use pyo3::exceptions;
15use pyo3::prelude::*;
16use pyo3::types::{PyDateTime, PyDelta, PyDict, PyList};
17use pythonize::pythonize;
18use std::{collections::HashMap, fs::File, io::Read};
19use tar::Archive;
20
21use crate::Pyo3Docker;
22
23#[pymodule]
24pub fn container(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
25    m.add_class::<Pyo3Containers>()?;
26    m.add_class::<Pyo3Container>()?;
27    Ok(())
28}
29
30/// Interface for managing Docker containers collection.
31#[derive(Debug)]
32#[pyclass(name = "Containers")]
33pub struct Pyo3Containers {
34    containers: Containers,
35    docker: docker_api::Docker,
36}
37
38/// Represents an individual Docker container.
39#[derive(Debug)]
40#[pyclass(name = "Container")]
41pub struct Pyo3Container {
42    container: Container,
43    docker: docker_api::Docker,
44}
45
46#[pymethods]
47impl Pyo3Containers {
48    #[new]
49    pub fn new(docker: Pyo3Docker) -> Self {
50        Pyo3Containers {
51            containers: Containers::new(docker.0.clone()),
52            docker: docker.0,
53        }
54    }
55
56    /// Get a specific container by ID or name.
57    ///
58    /// Args:
59    ///     id: Container ID or name
60    ///
61    /// Returns:
62    ///     Container: Container instance
63    fn get(&self, id: &str) -> Pyo3Container {
64        Pyo3Container {
65            container: self.containers.get(id),
66            docker: self.docker.clone(),
67        }
68    }
69
70    /// List containers.
71    ///
72    /// Args:
73    ///     all: Show all containers (default shows only running)
74    ///     since: Show containers created since this container ID
75    ///     before: Show containers created before this container ID
76    ///     sized: Include size information
77    ///
78    /// Returns:
79    ///     list[dict]: List of container information dictionaries
80    #[pyo3(signature = (all=None, since=None, before=None, sized=None))]
81    fn list(
82        &self,
83        all: Option<bool>,
84        since: Option<String>,
85        before: Option<String>,
86        sized: Option<bool>,
87    ) -> Py<PyAny> {
88        let mut builder = ContainerListOpts::builder();
89
90        bo_setter!(all, builder);
91        bo_setter!(since, builder);
92        bo_setter!(before, builder);
93        bo_setter!(sized, builder);
94
95        let cs = __containers_list(&self.containers, &builder.build());
96        pythonize_this!(cs)
97    }
98
99    /// Remove stopped containers.
100    ///
101    /// Returns:
102    ///     dict: Prune results including containers deleted and space reclaimed
103    fn prune(&self) -> PyResult<Py<PyAny>> {
104        let rv = __containers_prune(&self.containers, &Default::default());
105
106        match rv {
107            Ok(rv) => Ok(pythonize_this!(rv)),
108            Err(rv) => Err(py_sys_exception!(rv)),
109        }
110    }
111
112    /// Create a new container.
113    ///
114    /// Args:
115    ///     image: Image name to use for the container
116    ///     attach_stderr: Attach to stderr
117    ///     attach_stdin: Attach to stdin
118    ///     attach_stdout: Attach to stdout
119    ///     auto_remove: Automatically remove the container when it exits
120    ///     capabilities: List of Linux capabilities to add (e.g., ["NET_ADMIN", "SYS_TIME"])
121    ///     command: Command to run as list (e.g., ["/bin/sh", "-c", "echo hello"])
122    ///     cpu_shares: CPU shares (relative weight)
123    ///     cpus: Number of CPUs
124    ///     devices: List of device mappings, each a dict with PathOnHost, PathInContainer, CgroupPermissions
125    ///     entrypoint: Entrypoint as list (e.g., ["/bin/sh"])
126    ///     env: Environment variables as list (e.g., ["VAR=value"])
127    ///     expose: List of port mappings to expose as dicts with srcport, hostport, protocol
128    ///     extra_hosts: Extra host-to-IP mappings as list (e.g., ["hostname:192.168.1.1"])
129    ///     labels: Labels as dict (e.g., {"app": "myapp", "env": "prod"})
130    ///     links: Links to other containers as list
131    ///     log_driver: Logging driver (e.g., "json-file", "syslog")
132    ///     memory: Memory limit in bytes
133    ///     memory_swap: Total memory limit (memory + swap)
134    ///     name: Container name
135    ///     nano_cpus: CPU quota in units of 10^-9 CPUs
136    ///     network_mode: Network mode (e.g., "bridge", "host", "none")
137    ///     privileged: Give extended privileges to this container
138    ///     publish: List of ports to publish as dicts with port, protocol
139    ///     publish_all_ports: Publish all exposed ports to random ports
140    ///     restart_policy: Restart policy as dict with name and maximum_retry_count
141    ///     security_options: Security options as list (e.g., ["label=user:USER"])
142    ///     stop_signal: Signal to stop the container
143    ///     stop_signal_num: Signal number to stop the container
144    ///     stop_timeout: Timeout for stopping the container (timedelta)
145    ///     tty: Allocate a pseudo-TTY
146    ///     user: Username or UID
147    ///     userns_mode: User namespace mode
148    ///     volumes: Volume bindings as list (e.g., ["/host:/container:rw"])
149    ///     volumes_from: Mount volumes from other containers as list
150    ///     working_dir: Working directory inside the container
151    ///
152    /// Returns:
153    ///     Container: Created container instance
154    #[pyo3(signature = (image, *, attach_stderr=None, attach_stdin=None, attach_stdout=None, auto_remove=None, capabilities=None, command=None, cpu_shares=None, cpus=None, devices=None, entrypoint=None, env=None, expose=None, extra_hosts=None, labels=None, links=None, log_driver=None, memory=None, memory_swap=None, name=None, nano_cpus=None, network_mode=None, privileged=None, publish=None, publish_all_ports=None, restart_policy=None, security_options=None, stop_signal=None, stop_signal_num=None, stop_timeout=None, tty=None, user=None, userns_mode=None, volumes=None, volumes_from=None, working_dir=None))]
155    fn create(
156        &self,
157        image: &str,
158        attach_stderr: Option<bool>,
159        attach_stdin: Option<bool>,
160        attach_stdout: Option<bool>,
161        auto_remove: Option<bool>,
162        capabilities: Option<&Bound<'_, PyList>>,
163        command: Option<&Bound<'_, PyList>>,
164        cpu_shares: Option<u32>,
165        cpus: Option<f64>,
166        devices: Option<&Bound<'_, PyList>>,
167        entrypoint: Option<&Bound<'_, PyList>>,
168        env: Option<&Bound<'_, PyList>>,
169        expose: Option<&Bound<'_, PyList>>,
170        extra_hosts: Option<&Bound<'_, PyList>>,
171        labels: Option<&Bound<'_, PyDict>>,
172        links: Option<&Bound<'_, PyList>>,
173        log_driver: Option<&str>,
174        memory: Option<u64>,
175        memory_swap: Option<i64>,
176        name: Option<&str>,
177        nano_cpus: Option<u64>,
178        network_mode: Option<&str>,
179        privileged: Option<bool>,
180        publish: Option<&Bound<'_, PyList>>,
181        publish_all_ports: Option<bool>,
182        restart_policy: Option<&Bound<'_, PyDict>>, // name,maximum_retry_count,
183        security_options: Option<&Bound<'_, PyList>>,
184        stop_signal: Option<&str>,
185        stop_signal_num: Option<u64>,
186        stop_timeout: Option<&Bound<'_, PyDelta>>,
187        tty: Option<bool>,
188        user: Option<&str>,
189        userns_mode: Option<&str>,
190        volumes: Option<&Bound<'_, PyList>>,
191        volumes_from: Option<&Bound<'_, PyList>>,
192        working_dir: Option<&str>,
193    ) -> PyResult<Pyo3Container> {
194        let mut create_opts = ContainerCreateOpts::builder().image(image);
195
196        let links: Option<Vec<String>> = if links.is_some() {
197            links.unwrap().extract().unwrap()
198        } else {
199            None
200        };
201        let links: Option<Vec<&str>> = links
202            .as_ref()
203            .map(|v| v.iter().map(|s| s.as_str()).collect());
204
205        let capabilities_strings: Option<Vec<String>> = if capabilities.is_some() {
206            capabilities.unwrap().extract().unwrap()
207        } else {
208            None
209        };
210        let capabilities: Option<Vec<&str>> = capabilities_strings
211            .as_ref()
212            .map(|v| v.iter().map(|s| s.as_str()).collect());
213
214        let command_strings: Option<Vec<String>> = if command.is_some() {
215            command.unwrap().extract().unwrap()
216        } else {
217            None
218        };
219        let command: Option<Vec<&str>> = command_strings
220            .as_ref()
221            .map(|v| v.iter().map(|s| s.as_str()).collect());
222
223        let entrypoint_strings: Option<Vec<String>> = if entrypoint.is_some() {
224            entrypoint.unwrap().extract().unwrap()
225        } else {
226            None
227        };
228        let entrypoint: Option<Vec<&str>> = entrypoint_strings
229            .as_ref()
230            .map(|v| v.iter().map(|s| s.as_str()).collect());
231
232        let env_strings: Option<Vec<String>> = if env.is_some() {
233            env.unwrap().extract().unwrap()
234        } else {
235            None
236        };
237        let env: Option<Vec<&str>> = env_strings
238            .as_ref()
239            .map(|v| v.iter().map(|s| s.as_str()).collect());
240
241        let extra_hosts_strings: Option<Vec<String>> = if extra_hosts.is_some() {
242            extra_hosts.unwrap().extract().unwrap()
243        } else {
244            None
245        };
246        let extra_hosts: Option<Vec<&str>> = extra_hosts_strings
247            .as_ref()
248            .map(|v| v.iter().map(|s| s.as_str()).collect());
249
250        let security_options_strings: Option<Vec<String>> = if security_options.is_some() {
251            security_options.unwrap().extract().unwrap()
252        } else {
253            None
254        };
255        let security_options: Option<Vec<&str>> = security_options_strings
256            .as_ref()
257            .map(|v| v.iter().map(|s| s.as_str()).collect());
258
259        let volumes_strings: Option<Vec<String>> = if volumes.is_some() {
260            volumes.unwrap().extract().unwrap()
261        } else {
262            None
263        };
264        let volumes: Option<Vec<&str>> = volumes_strings
265            .as_ref()
266            .map(|v| v.iter().map(|s| s.as_str()).collect());
267
268        let volumes_from_strings: Option<Vec<String>> = if volumes_from.is_some() {
269            volumes_from.unwrap().extract().unwrap()
270        } else {
271            None
272        };
273        let volumes_from: Option<Vec<&str>> = volumes_from_strings
274            .as_ref()
275            .map(|v| v.iter().map(|s| s.as_str()).collect());
276
277        let devices_vec: Option<Vec<HashMap<String, String>>> = if devices.is_some() {
278            let list = devices.unwrap();
279            let mut result = Vec::new();
280            for item in list.iter() {
281                let dict: HashMap<String, String> = item.extract().unwrap();
282                result.push(dict);
283            }
284            Some(result)
285        } else {
286            None
287        };
288        let devices = devices_vec;
289
290        let labels_map: Option<HashMap<String, String>> = if labels.is_some() {
291            Some(labels.unwrap().extract().unwrap())
292        } else {
293            None
294        };
295        let labels: Option<HashMap<&str, &str>> = labels_map
296            .as_ref()
297            .map(|m| m.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect());
298
299        let stop_timeout_duration: Option<std::time::Duration> =
300            stop_timeout.map(|st| st.extract::<chrono::Duration>().unwrap().to_std().unwrap());
301        let stop_timeout = stop_timeout_duration;
302
303        bo_setter!(attach_stderr, create_opts);
304        bo_setter!(attach_stdin, create_opts);
305        bo_setter!(attach_stdout, create_opts);
306        bo_setter!(auto_remove, create_opts);
307        bo_setter!(cpu_shares, create_opts);
308        bo_setter!(cpus, create_opts);
309        bo_setter!(log_driver, create_opts);
310        bo_setter!(memory, create_opts);
311        bo_setter!(memory_swap, create_opts);
312        bo_setter!(name, create_opts);
313        bo_setter!(nano_cpus, create_opts);
314        bo_setter!(network_mode, create_opts);
315        bo_setter!(privileged, create_opts);
316        bo_setter!(stop_signal, create_opts);
317        bo_setter!(stop_signal_num, create_opts);
318        bo_setter!(tty, create_opts);
319        bo_setter!(user, create_opts);
320        bo_setter!(userns_mode, create_opts);
321        bo_setter!(working_dir, create_opts);
322
323        bo_setter!(devices, create_opts);
324        bo_setter!(links, create_opts);
325        bo_setter!(capabilities, create_opts);
326        bo_setter!(command, create_opts);
327        bo_setter!(entrypoint, create_opts);
328        bo_setter!(env, create_opts);
329        bo_setter!(extra_hosts, create_opts);
330        bo_setter!(security_options, create_opts);
331        bo_setter!(volumes, create_opts);
332        bo_setter!(volumes_from, create_opts);
333
334        bo_setter!(labels, create_opts);
335        bo_setter!(stop_timeout, create_opts);
336
337        // Handle expose - expects list of dicts like [{"srcport": 8080, "protocol": "tcp", "hostport": 8000}]
338        if let Some(expose_list) = expose {
339            for item in expose_list.iter() {
340                let port_dict: &Bound<'_, PyDict> = item.cast()?;
341                let srcport: u32 = port_dict
342                    .get_item("srcport")?
343                    .expect("srcport required")
344                    .extract()?;
345                let hostport: u32 = port_dict
346                    .get_item("hostport")?
347                    .expect("hostport required")
348                    .extract()?;
349                let protocol: String = match port_dict.get_item("protocol")? {
350                    Some(p) => p.extract()?,
351                    None => "tcp".to_string(),
352                };
353
354                let publish_port = match protocol.as_str() {
355                    "tcp" => PublishPort::tcp(srcport),
356                    "udp" => PublishPort::udp(srcport),
357                    "sctp" => PublishPort::sctp(srcport),
358                    _ => {
359                        return Err(exceptions::PyValueError::new_err(format!(
360                            "unknown protocol: {}",
361                            protocol
362                        )))
363                    }
364                };
365
366                create_opts = create_opts.expose(publish_port, hostport);
367            }
368        }
369
370        // Handle publish - expects list of dicts like [{"port": 8080, "protocol": "tcp"}]
371        if let Some(publish_list) = publish {
372            for item in publish_list.iter() {
373                let port_dict: &Bound<'_, PyDict> = item.cast()?;
374                let port: u32 = port_dict
375                    .get_item("port")?
376                    .expect("port required")
377                    .extract()?;
378                let protocol: String = match port_dict.get_item("protocol")? {
379                    Some(p) => p.extract()?,
380                    None => "tcp".to_string(),
381                };
382
383                let publish_port = match protocol.as_str() {
384                    "tcp" => PublishPort::tcp(port),
385                    "udp" => PublishPort::udp(port),
386                    "sctp" => PublishPort::sctp(port),
387                    _ => {
388                        return Err(exceptions::PyValueError::new_err(format!(
389                            "unknown protocol: {}",
390                            protocol
391                        )))
392                    }
393                };
394
395                create_opts = create_opts.publish(publish_port);
396            }
397        }
398
399        if publish_all_ports.is_some() && publish_all_ports.unwrap() {
400            create_opts = create_opts.publish_all_ports();
401        }
402
403        if restart_policy.is_some() {
404            let policy_dict = restart_policy.unwrap();
405            let name = policy_dict
406                .get_item("name")
407                .unwrap_or(None)
408                .expect("restart_policy requires 'name' key")
409                .extract::<String>()
410                .unwrap();
411            let max_retry = policy_dict
412                .get_item("maximum_retry_count")
413                .unwrap_or(None)
414                .map(|v| v.extract::<u64>().unwrap())
415                .unwrap_or(0);
416
417            create_opts = create_opts.restart_policy(&name, max_retry);
418        }
419
420        // bo_setter!(expose, create_opts);
421        // bo_setter!(publish, create_opts);
422
423        let rv = __containers_create(&self.containers, &create_opts.build());
424        match rv {
425            Ok(container) => Ok(Pyo3Container {
426                container,
427                docker: self.docker.clone(),
428            }),
429            Err(rv) => Err(py_sys_exception!(rv)),
430        }
431    }
432}
433
434#[tokio::main]
435async fn __containers_list(
436    containers: &Containers,
437    opts: &ContainerListOpts,
438) -> Vec<ContainerSummary> {
439    let x = containers.list(opts).await;
440    x.unwrap()
441}
442
443#[tokio::main]
444async fn __containers_prune(
445    containers: &Containers,
446    opts: &ContainerPruneOpts,
447) -> Result<ContainerPrune200Response, docker_api::Error> {
448    containers.prune(opts).await
449}
450
451#[tokio::main]
452async fn __containers_create(
453    containers: &Containers,
454    opts: &ContainerCreateOpts,
455) -> Result<Container, docker_api::Error> {
456    containers.create(opts).await
457}
458
459#[pymethods]
460impl Pyo3Container {
461    #[new]
462    fn new(docker: Pyo3Docker, id: String) -> Self {
463        Pyo3Container {
464            container: Container::new(docker.0.clone(), id),
465            docker: docker.0,
466        }
467    }
468
469    /// Get the container ID.
470    ///
471    /// Returns:
472    ///     str: Container ID
473    fn id(&self) -> String {
474        self.container.id().to_string()
475    }
476
477    /// Inspect the container to get detailed information.
478    ///
479    /// Returns:
480    ///     dict: Detailed container information including config, state, mounts, etc.
481    fn inspect(&self) -> PyResult<Py<PyAny>> {
482        let ci = __container_inspect(&self.container);
483        Ok(pythonize_this!(ci))
484    }
485
486    /// Get container logs.
487    ///
488    /// Args:
489    ///     stdout: Include stdout
490    ///     stderr: Include stderr
491    ///     timestamps: Include timestamps
492    ///     n_lines: Number of lines to return from the end of logs
493    ///     all: Return all logs
494    ///     since: Only return logs since this datetime
495    ///
496    /// Returns:
497    ///     str: Container logs
498    #[pyo3(signature = (stdout=None, stderr=None, timestamps=None, n_lines=None, all=None, since=None))]
499    fn logs(
500        &self,
501        stdout: Option<bool>,
502        stderr: Option<bool>,
503        timestamps: Option<bool>,
504        n_lines: Option<usize>,
505        all: Option<bool>,
506        since: Option<&Bound<'_, PyDateTime>>,
507    ) -> String {
508        let mut log_opts = LogsOpts::builder();
509
510        bo_setter!(stdout, log_opts);
511        bo_setter!(stderr, log_opts);
512        bo_setter!(timestamps, log_opts);
513        bo_setter!(n_lines, log_opts);
514
515        if all.is_some() && all.unwrap() {
516            // all needs to be called w/o a value
517            log_opts = log_opts.all();
518        }
519
520        if since.is_some() {
521            let rs_since: DateTime<Utc> = since.unwrap().extract().unwrap();
522            log_opts = log_opts.since(&rs_since);
523        }
524
525        __container_logs(&self.container, &log_opts.build())
526    }
527
528    /// Remove the container (not implemented yet).
529    fn remove(&self) -> PyResult<()> {
530        Err(exceptions::PyNotImplementedError::new_err(
531            "This method is not available yet.",
532        ))
533    }
534
535    /// Delete the container.
536    ///
537    /// Returns:
538    ///     None
539    ///
540    /// Raises:
541    ///     SystemError: If the container cannot be deleted
542    fn delete(&self) -> PyResult<()> {
543        let rv = __container_delete(&self.container);
544        if rv.is_ok() {
545            Ok(())
546        } else {
547            Err(exceptions::PySystemError::new_err(
548                "Failed to delete container.",
549            ))
550        }
551    }
552
553    // fn top(&self) -> PyResult<()> {
554    //     Err(exceptions::PyNotImplementedError::new_err(
555    //         "This method is not available yet.",
556    //     ))
557    // }
558
559    // fn export(&self, docker_path: &str, local_path: &str) -> PyResult<()> {
560    //     let bytes = self.0.export();
561    //     let mut archive = Archive::new(&bytes[..]);
562    //     archive.unpack(local_path);
563
564    //     Ok(())
565    // }
566
567    /// Start the container.
568    ///
569    /// Returns:
570    ///     None
571    ///
572    /// Raises:
573    ///     SystemError: If the container cannot be started
574    fn start(&self) -> PyResult<()> {
575        let rv = __container_start(&self.container);
576
577        match rv {
578            Ok(_rv) => Ok(()),
579            Err(_rv) => Err(exceptions::PySystemError::new_err(
580                "Failed to start container",
581            )),
582        }
583    }
584
585    /// Stop the container.
586    ///
587    /// Args:
588    ///     wait: Time to wait before killing the container (timedelta)
589    ///
590    /// Returns:
591    ///     None
592    ///
593    /// Raises:
594    ///     SystemError: If the container cannot be stopped
595    fn stop(&self, wait: Option<&Bound<'_, PyDelta>>) -> PyResult<()> {
596        let wait: Option<std::time::Duration> = wait.map(|wait| {
597            wait.extract::<chrono::Duration>()
598                .unwrap()
599                .to_std()
600                .unwrap()
601        });
602
603        let rv = __container_stop(&self.container, wait);
604        match rv {
605            Ok(_rv) => Ok(()),
606            Err(_rv) => Err(exceptions::PySystemError::new_err(
607                "Failed to start container",
608            )),
609        }
610    }
611
612    /// Restart the container.
613    ///
614    /// Args:
615    ///     wait: Time to wait before killing the container (timedelta)
616    ///
617    /// Returns:
618    ///     None
619    ///
620    /// Raises:
621    ///     SystemError: If the container cannot be restarted
622    fn restart(&self, wait: Option<&Bound<'_, PyDelta>>) -> PyResult<()> {
623        let wait: Option<std::time::Duration> = wait.map(|wait| {
624            wait.extract::<chrono::Duration>()
625                .unwrap()
626                .to_std()
627                .unwrap()
628        });
629
630        let rv = __container_restart(&self.container, wait);
631        match rv {
632            Ok(_rv) => Ok(()),
633            Err(_rv) => Err(exceptions::PySystemError::new_err(
634                "Failed to stop container",
635            )),
636        }
637    }
638
639    /// Kill the container by sending a signal.
640    ///
641    /// Args:
642    ///     signal: Signal to send (e.g., "SIGKILL", "SIGTERM")
643    ///
644    /// Returns:
645    ///     None
646    ///
647    /// Raises:
648    ///     SystemError: If the container cannot be killed
649    fn kill(&self, signal: Option<&str>) -> PyResult<()> {
650        let rv = __container_kill(&self.container, signal);
651        match rv {
652            Ok(_rv) => Ok(()),
653            Err(_rv) => Err(exceptions::PySystemError::new_err(
654                "Failed to kill container",
655            )),
656        }
657    }
658
659    /// Rename the container.
660    ///
661    /// Args:
662    ///     name: New name for the container
663    ///
664    /// Returns:
665    ///     None
666    ///
667    /// Raises:
668    ///     SystemError: If the container cannot be renamed
669    fn rename(&self, name: &str) -> PyResult<()> {
670        let rv = __container_rename(&self.container, name);
671        match rv {
672            Ok(_rv) => Ok(()),
673            Err(_rv) => Err(exceptions::PySystemError::new_err(
674                "Failed to rename container",
675            )),
676        }
677    }
678
679    /// Pause the container.
680    ///
681    /// Returns:
682    ///     None
683    ///
684    /// Raises:
685    ///     SystemError: If the container cannot be paused
686    fn pause(&self) -> PyResult<()> {
687        let rv = __container_pause(&self.container);
688        match rv {
689            Ok(_rv) => Ok(()),
690            Err(_rv) => Err(exceptions::PySystemError::new_err(
691                "Failed to pause container",
692            )),
693        }
694    }
695
696    /// Unpause the container.
697    ///
698    /// Returns:
699    ///     None
700    ///
701    /// Raises:
702    ///     SystemError: If the container cannot be unpaused
703    fn unpause(&self) -> PyResult<()> {
704        let rv = __container_unpause(&self.container);
705        match rv {
706            Ok(_rv) => Ok(()),
707            Err(_rv) => Err(exceptions::PySystemError::new_err(
708                "Failed to unpause container",
709            )),
710        }
711    }
712
713    /// Wait for the container to stop.
714    ///
715    /// Returns:
716    ///     dict: Wait response including status code
717    fn wait(&self) -> Py<PyAny> {
718        let rv = __container_wait(&self.container).unwrap();
719        pythonize_this!(rv)
720    }
721
722    /// Get container resource usage statistics.
723    ///
724    /// Args:
725    ///     stream: If True, continuously stream stats. If False (default), return single snapshot.
726    ///
727    /// Returns:
728    ///     dict: Container statistics including CPU, memory, network, and I/O usage.
729    ///           If stream=True, returns a list of stats snapshots.
730    ///
731    /// Raises:
732    ///     SystemError: If stats cannot be retrieved
733    #[pyo3(signature = (stream=None))]
734    fn stats(&self, stream: Option<bool>) -> PyResult<Py<PyAny>> {
735        let stream = stream.unwrap_or(false);
736        let rv = __container_stats(&self.container, stream);
737        match rv {
738            Ok(rv) => Ok(pythonize_this!(rv)),
739            Err(rv) => Err(py_sys_exception!(rv)),
740        }
741    }
742
743    /// Attach to the container's standard streams.
744    ///
745    /// This method attaches to the container and collects output from stdout/stderr.
746    /// It's designed for short-lived connections to collect output.
747    ///
748    /// Returns:
749    ///     str: Combined stdout and stderr output from the container
750    ///
751    /// Raises:
752    ///     SystemError: If attach fails
753    fn attach(&self) -> PyResult<String> {
754        let rv = __container_attach(&self.container);
755        match rv {
756            Ok(rv) => Ok(rv),
757            Err(rv) => Err(py_sys_exception!(rv)),
758        }
759    }
760
761    /// Get filesystem changes made to the container.
762    ///
763    /// Returns a list of changes made to the container's filesystem.
764    /// Each change includes the path and kind of change (Added, Modified, Deleted).
765    ///
766    /// Returns:
767    ///     list[dict] | None: List of filesystem changes, or None if no changes
768    ///
769    /// Raises:
770    ///     SystemError: If changes cannot be retrieved
771    fn changes(&self) -> PyResult<Py<PyAny>> {
772        let rv = __container_changes(&self.container);
773        match rv {
774            Ok(rv) => Ok(pythonize_this!(rv)),
775            Err(rv) => Err(py_sys_exception!(rv)),
776        }
777    }
778
779    /// Export the container as a tarball.
780    ///
781    /// Args:
782    ///     path: Local path to save the exported tarball
783    ///
784    /// Returns:
785    ///     None
786    ///
787    /// Raises:
788    ///     SystemError: If export fails
789    fn export(&self, path: &str) -> PyResult<()> {
790        let rv = __container_export(&self.container);
791        match rv {
792            Ok(bytes) => {
793                use std::io::Write;
794                let mut file = File::create(path)
795                    .map_err(|e| exceptions::PySystemError::new_err(format!("{e}")))?;
796                file.write_all(&bytes)
797                    .map_err(|e| exceptions::PySystemError::new_err(format!("{e}")))?;
798                Ok(())
799            }
800            Err(rv) => Err(py_sys_exception!(rv)),
801        }
802    }
803
804    /// Get running processes in the container.
805    ///
806    /// Args:
807    ///     ps_args: Arguments to pass to ps command (e.g., "aux", "-ef")
808    ///
809    /// Returns:
810    ///     dict: Process information including titles and process list
811    ///
812    /// Raises:
813    ///     SystemError: If top cannot be executed (Unix only)
814    #[pyo3(signature = (ps_args=None))]
815    fn top(&self, ps_args: Option<&str>) -> PyResult<Py<PyAny>> {
816        let rv = __container_top(&self.container, ps_args);
817        match rv {
818            Ok(rv) => Ok(pythonize_this!(rv)),
819            Err(rv) => Err(py_sys_exception!(rv)),
820        }
821    }
822
823    /// Execute a command in the running container.
824    ///
825    /// Args:
826    ///     command: Command to execute as list (e.g., ["/bin/sh", "-c", "ls"])
827    ///     env: Environment variables as list (e.g., ["VAR=value"])
828    ///     attach_stdout: Attach to stdout
829    ///     attach_stderr: Attach to stderr
830    ///     detach_keys: Override key sequence for detaching
831    ///     tty: Allocate a pseudo-TTY
832    ///     privileged: Run with extended privileges
833    ///     user: Username or UID
834    ///     working_dir: Working directory for the exec session
835    ///
836    /// Returns:
837    ///     None
838    ///
839    /// Raises:
840    ///     SystemError: If the command cannot be executed
841    fn exec(
842        &self,
843        command: &Bound<'_, PyList>,
844        env: Option<&Bound<'_, PyList>>,
845        attach_stdout: Option<bool>,
846        attach_stderr: Option<bool>,
847        detach_keys: Option<&str>,
848        tty: Option<bool>,
849        privileged: Option<bool>,
850        user: Option<&str>,
851        working_dir: Option<&str>,
852    ) -> PyResult<()> {
853        let command_strings: Vec<String> = command.extract().unwrap();
854        let command: Vec<&str> = command_strings.iter().map(|s| s.as_str()).collect();
855        let mut exec_opts = ExecCreateOpts::builder().command(command);
856
857        if env.is_some() {
858            let env_strings: Vec<String> = env.unwrap().extract().unwrap();
859            let env: Vec<&str> = env_strings.iter().map(|s| s.as_str()).collect();
860            exec_opts = exec_opts.env(env);
861        }
862
863        bo_setter!(attach_stdout, exec_opts);
864        bo_setter!(attach_stderr, exec_opts);
865        bo_setter!(tty, exec_opts);
866        bo_setter!(detach_keys, exec_opts);
867        bo_setter!(privileged, exec_opts);
868        bo_setter!(user, exec_opts);
869        bo_setter!(working_dir, exec_opts);
870
871        let rv = __container_exec(&self.container, exec_opts.build());
872        let rv = rv.unwrap();
873        match rv {
874            Ok(_rv) => Ok(()),
875            Err(rv) => Err(exceptions::PySystemError::new_err(format!(
876                "Failed to exec container {rv}"
877            ))),
878        }
879    }
880
881    /// Create an exec instance without starting it.
882    ///
883    /// Use this when you need to inspect or resize the exec session before or during execution.
884    /// Returns the exec ID which can be used to create an Exec object.
885    ///
886    /// Args:
887    ///     command: Command to execute as list (e.g., ["/bin/sh", "-c", "ls"])
888    ///     env: Environment variables as list (e.g., ["VAR=value"])
889    ///     attach_stdout: Attach to stdout
890    ///     attach_stderr: Attach to stderr
891    ///     detach_keys: Override key sequence for detaching
892    ///     tty: Allocate a pseudo-TTY (required for resize)
893    ///     privileged: Run with extended privileges
894    ///     user: Username or UID
895    ///     working_dir: Working directory for the exec session
896    ///
897    /// Returns:
898    ///     str: Exec instance ID (use with Exec(docker, id) to get the exec object)
899    ///
900    /// Raises:
901    ///     SystemError: If the exec cannot be created
902    ///
903    /// Example:
904    ///     >>> exec_id = container.exec_create(["/bin/bash"], tty=True)
905    ///     >>> exec_obj = Exec(docker, exec_id)
906    ///     >>> exec_obj.resize(80, 24)
907    ///     >>> exec_obj.inspect()
908    #[pyo3(signature = (command, env=None, attach_stdout=None, attach_stderr=None, detach_keys=None, tty=None, privileged=None, user=None, working_dir=None))]
909    fn exec_create(
910        &self,
911        command: &Bound<'_, PyList>,
912        env: Option<&Bound<'_, PyList>>,
913        attach_stdout: Option<bool>,
914        attach_stderr: Option<bool>,
915        detach_keys: Option<&str>,
916        tty: Option<bool>,
917        privileged: Option<bool>,
918        user: Option<&str>,
919        working_dir: Option<&str>,
920    ) -> PyResult<String> {
921        let command_strings: Vec<String> = command.extract().unwrap();
922        let command: Vec<&str> = command_strings.iter().map(|s| s.as_str()).collect();
923        let mut exec_opts = ExecCreateOpts::builder().command(command);
924
925        if env.is_some() {
926            let env_strings: Vec<String> = env.unwrap().extract().unwrap();
927            let env: Vec<&str> = env_strings.iter().map(|s| s.as_str()).collect();
928            exec_opts = exec_opts.env(env);
929        }
930
931        bo_setter!(attach_stdout, exec_opts);
932        bo_setter!(attach_stderr, exec_opts);
933        bo_setter!(tty, exec_opts);
934        bo_setter!(detach_keys, exec_opts);
935        bo_setter!(privileged, exec_opts);
936        bo_setter!(user, exec_opts);
937        bo_setter!(working_dir, exec_opts);
938
939        let rv = __container_exec_create(
940            self.docker.clone(),
941            self.container.id().as_ref(),
942            exec_opts.build(),
943        );
944        match rv {
945            Ok(exec_id) => Ok(exec_id),
946            Err(rv) => Err(py_sys_exception!(rv)),
947        }
948    }
949
950    fn copy_from(&self, src: &str, dst: &str) -> PyResult<()> {
951        let rv = __container_copy_from(&self.container, src);
952
953        match rv {
954            Ok(rv) => {
955                let mut archive = Archive::new(&rv[..]);
956                let r = archive.unpack(dst);
957                match r {
958                    Ok(_r) => Ok(()),
959                    Err(r) => Err(exceptions::PySystemError::new_err(format!("{r}"))),
960                }
961            }
962            Err(rv) => Err(exceptions::PySystemError::new_err(format!("{rv}"))),
963        }
964    }
965
966    fn copy_file_into(&self, src: &str, dst: &str) -> PyResult<()> {
967        let mut file = File::open(src).unwrap();
968        let mut bytes = Vec::new();
969        file.read_to_end(&mut bytes)
970            .expect("Cannot read file on the localhost.");
971
972        let rv = __container_copy_file_into(&self.container, dst, &bytes);
973
974        match rv {
975            Ok(_rv) => Ok(()),
976            Err(rv) => Err(exceptions::PySystemError::new_err(format!("{rv}"))),
977        }
978    }
979
980    fn stat_file(&self, path: &str) -> Py<PyAny> {
981        let rv = __container_stat_file(&self.container, path).unwrap();
982        pythonize_this!(rv)
983    }
984
985    /// Create a new image from the container.
986    ///
987    /// Args:
988    ///     repo: Repository name for the new image (e.g., "myimage")
989    ///     tag: Tag for the new image (e.g., "v1.0")
990    ///     comment: Commit message
991    ///     author: Author of the commit (e.g., "John Doe <john@example.com>")
992    ///     pause: Pause the container during commit
993    ///     changes: Dockerfile instruction to apply (e.g., "CMD /bin/bash")
994    ///
995    /// Returns:
996    ///     str: ID of the created image
997    ///
998    /// Raises:
999    ///     SystemError: If commit fails
1000    #[pyo3(signature = (repo=None, tag=None, comment=None, author=None, pause=None, changes=None))]
1001    fn commit(
1002        &self,
1003        repo: Option<&str>,
1004        tag: Option<&str>,
1005        comment: Option<&str>,
1006        author: Option<&str>,
1007        pause: Option<bool>,
1008        changes: Option<&str>,
1009    ) -> PyResult<String> {
1010        let mut opts = ContainerCommitOpts::builder();
1011
1012        bo_setter!(repo, opts);
1013        bo_setter!(tag, opts);
1014        bo_setter!(comment, opts);
1015        bo_setter!(author, opts);
1016        bo_setter!(pause, opts);
1017        bo_setter!(changes, opts);
1018
1019        let rv = __container_commit(&self.container, &opts.build());
1020        match rv {
1021            Ok(rv) => Ok(rv),
1022            Err(rv) => Err(py_sys_exception!(rv)),
1023        }
1024    }
1025
1026    fn __repr__(&self) -> String {
1027        let inspect = __container_inspect(&self.container);
1028        format!(
1029            "Container(id: {}, name: {}, status: {})",
1030            inspect.id.unwrap(),
1031            inspect.name.unwrap(),
1032            inspect.state.unwrap().status.unwrap()
1033        )
1034    }
1035
1036    fn __string__(&self) -> String {
1037        self.__repr__()
1038    }
1039}
1040
1041#[tokio::main]
1042async fn __container_inspect(container: &Container) -> ContainerInspect200Response {
1043    let c = container.inspect().await;
1044    c.unwrap()
1045}
1046
1047#[tokio::main]
1048async fn __container_logs(container: &Container, log_opts: &LogsOpts) -> String {
1049    let log_stream = container.logs(log_opts);
1050
1051    let log = log_stream
1052        .map(|chunk| match chunk {
1053            Ok(chunk) => chunk.to_vec(),
1054            Err(e) => {
1055                eprintln!("Error: {e}");
1056                vec![]
1057            }
1058        })
1059        .collect::<Vec<_>>()
1060        .await
1061        .into_iter()
1062        .flatten()
1063        .collect::<Vec<_>>();
1064
1065    format!("{}", String::from_utf8_lossy(&log))
1066}
1067
1068#[tokio::main]
1069async fn __container_delete(container: &Container) -> Result<String, docker_api::Error> {
1070    container.delete().await
1071}
1072
1073#[tokio::main]
1074async fn __container_start(container: &Container) -> Result<(), docker_api::Error> {
1075    container.start().await
1076}
1077
1078#[tokio::main]
1079async fn __container_stop(
1080    container: &Container,
1081    wait: Option<std::time::Duration>,
1082) -> Result<(), docker_api::Error> {
1083    let mut opts = ContainerStopOpts::builder();
1084    if let Some(w) = wait {
1085        opts = opts.wait(w);
1086    }
1087    container.stop(&opts.build()).await
1088}
1089
1090#[tokio::main]
1091async fn __container_restart(
1092    container: &Container,
1093    wait: Option<std::time::Duration>,
1094) -> Result<(), docker_api::Error> {
1095    let mut opts = ContainerRestartOpts::builder();
1096    if let Some(w) = wait {
1097        opts = opts.wait(w);
1098    }
1099    container.restart(&opts.build()).await
1100}
1101
1102#[tokio::main]
1103async fn __container_kill(
1104    container: &Container,
1105    signal: Option<&str>,
1106) -> Result<(), docker_api::Error> {
1107    container.kill(signal).await
1108}
1109
1110#[tokio::main]
1111async fn __container_rename(container: &Container, name: &str) -> Result<(), docker_api::Error> {
1112    container.rename(name).await
1113}
1114
1115#[tokio::main]
1116async fn __container_pause(container: &Container) -> Result<(), docker_api::Error> {
1117    container.pause().await
1118}
1119
1120#[tokio::main]
1121async fn __container_unpause(container: &Container) -> Result<(), docker_api::Error> {
1122    container.unpause().await
1123}
1124
1125#[tokio::main]
1126async fn __container_wait(
1127    container: &Container,
1128) -> Result<ContainerWaitResponse, docker_api::Error> {
1129    container.wait().await
1130}
1131
1132#[tokio::main]
1133async fn __container_stats(
1134    container: &Container,
1135    stream: bool,
1136) -> Result<serde_json::Value, docker_api::Error> {
1137    let mut stats_stream = container.stats();
1138
1139    if stream {
1140        // Collect multiple stats snapshots (limit to avoid infinite stream)
1141        let mut stats_vec: Vec<serde_json::Value> = Vec::new();
1142        let mut count = 0;
1143        while let Some(stat_result) = stats_stream.next().await {
1144            match stat_result {
1145                Ok(stat) => {
1146                    stats_vec.push(stat);
1147                    count += 1;
1148                    if count >= 10 {
1149                        // Limit to 10 snapshots for streaming mode
1150                        break;
1151                    }
1152                }
1153                Err(e) => return Err(e),
1154            }
1155        }
1156        Ok(serde_json::Value::Array(stats_vec))
1157    } else {
1158        // Return single snapshot
1159        match stats_stream.next().await {
1160            Some(Ok(stat)) => Ok(stat),
1161            Some(Err(e)) => Err(e),
1162            None => Ok(serde_json::Value::Null),
1163        }
1164    }
1165}
1166
1167#[tokio::main]
1168async fn __container_attach(container: &Container) -> Result<String, docker_api::Error> {
1169    use futures_util::StreamExt;
1170
1171    let mut multiplexer = container.attach().await?;
1172    let mut output = Vec::new();
1173
1174    // Collect output chunks with a timeout/limit to avoid hanging
1175    let mut count = 0;
1176    while let Some(chunk_result) = multiplexer.next().await {
1177        match chunk_result {
1178            Ok(chunk) => {
1179                output.extend_from_slice(&chunk.to_vec());
1180                count += 1;
1181                if count >= 100 {
1182                    // Limit chunks to avoid infinite streaming
1183                    break;
1184                }
1185            }
1186            Err(_) => break,
1187        }
1188    }
1189
1190    Ok(String::from_utf8_lossy(&output).to_string())
1191}
1192
1193#[tokio::main]
1194async fn __container_changes(
1195    container: &Container,
1196) -> Result<Option<ContainerChanges200Response>, docker_api::Error> {
1197    container.changes().await
1198}
1199
1200#[tokio::main]
1201async fn __container_export(container: &Container) -> Result<Vec<u8>, docker_api::Error> {
1202    container.export().try_concat().await
1203}
1204
1205#[tokio::main]
1206async fn __container_top(
1207    container: &Container,
1208    ps_args: Option<&str>,
1209) -> Result<ContainerTop200Response, docker_api::Error> {
1210    container.top(ps_args).await
1211}
1212
1213#[tokio::main]
1214async fn __container_commit(
1215    container: &Container,
1216    opts: &ContainerCommitOpts,
1217) -> Result<String, docker_api::Error> {
1218    container.commit(opts, None).await
1219}
1220
1221#[tokio::main]
1222async fn __container_exec(
1223    container: &Container,
1224    exec_opts: ExecCreateOpts,
1225) -> Option<Result<TtyChunk, docker_api::conn::Error>> {
1226    let start_opts = ExecStartOpts::builder().build();
1227    match container.exec(&exec_opts, &start_opts).await {
1228        Ok(mut multiplexer) => multiplexer.next().await,
1229        Err(_) => None,
1230    }
1231}
1232
1233#[tokio::main]
1234async fn __container_copy_from(
1235    container: &Container,
1236    path: &str,
1237) -> Result<Vec<u8>, docker_api::Error> {
1238    container.copy_from(path).try_concat().await
1239}
1240
1241#[tokio::main]
1242async fn __container_copy_file_into(
1243    container: &Container,
1244    dst: &str,
1245    bytes: &Vec<u8>,
1246) -> Result<(), docker_api::Error> {
1247    container.copy_file_into(dst, bytes).await
1248}
1249
1250#[tokio::main]
1251async fn __container_stat_file(
1252    container: &Container,
1253    src: &str,
1254) -> Result<String, docker_api::Error> {
1255    container.stat_file(src).await
1256}
1257
1258#[tokio::main]
1259async fn __container_exec_create(
1260    docker: docker_api::Docker,
1261    container_id: &str,
1262    exec_opts: ExecCreateOpts,
1263) -> Result<String, docker_api::Error> {
1264    // Create the exec instance
1265    let exec = Exec::create(docker, container_id, &exec_opts).await?;
1266    // Inspect to get the ID (the only reliable way to get the ID from the Exec struct)
1267    let inspect = exec.inspect().await?;
1268    Ok(inspect.id.unwrap_or_default())
1269}