11import contextlib
2+ import io
3+ import pathlib
24import sys
5+ import tarfile
36from os import PathLike
47from socket import socket
58from types import TracebackType
1619from testcontainers .core .config import testcontainers_config as c
1720from testcontainers .core .docker_client import DockerClient
1821from testcontainers .core .exceptions import ContainerConnectException , ContainerStartException
22+ from testcontainers .core .inspect import ContainerInspectInfo
1923from testcontainers .core .labels import LABEL_SESSION_ID , SESSION_ID
2024from testcontainers .core .network import Network
25+ from testcontainers .core .transferable import Transferable , TransferSpec , build_transfer_tar
2126from testcontainers .core .utils import is_arm , setup_logger
2227from testcontainers .core .wait_strategies import LogMessageWaitStrategy
2328from testcontainers .core .waiting_utils import WaitStrategy
@@ -69,6 +74,7 @@ def __init__(
6974 network : Optional [Network ] = None ,
7075 network_aliases : Optional [list [str ]] = None ,
7176 _wait_strategy : Optional [WaitStrategy ] = None ,
77+ transferables : Optional [list [TransferSpec ]] = None ,
7278 ** kwargs : Any ,
7379 ) -> None :
7480 self .env = env or {}
@@ -82,6 +88,8 @@ def __init__(
8288 for vol in volumes :
8389 self .with_volume_mapping (* vol )
8490
91+ self .tmpfs : dict [str , str ] = {}
92+
8593 self .image = image
8694 self ._docker = DockerClient (** (docker_client_kw or {}))
8795 self ._container : Optional [Container ] = None
@@ -97,6 +105,12 @@ def __init__(
97105
98106 self ._kwargs = kwargs
99107 self ._wait_strategy : Optional [WaitStrategy ] = _wait_strategy
108+ self ._cached_container_info : Optional [ContainerInspectInfo ] = None
109+
110+ self ._transferable_specs : list [TransferSpec ] = []
111+ if transferables :
112+ for t in transferables :
113+ self .with_copy_into_container (* t )
100114
101115 def with_env (self , key : str , value : str ) -> Self :
102116 self .env [key ] = value
@@ -198,13 +212,18 @@ def start(self) -> Self:
198212 ports = cast ("dict[int, Optional[int]]" , self .ports ),
199213 name = self ._name ,
200214 volumes = self .volumes ,
215+ tmpfs = self .tmpfs ,
201216 ** {** network_kwargs , ** self ._kwargs },
202217 )
203218
204219 if self ._wait_strategy is not None :
205220 self ._wait_strategy .wait_until_ready (self )
206221
207222 logger .info ("Container started: %s" , self ._container .short_id )
223+
224+ for t in self ._transferable_specs :
225+ self ._transfer_into_container (* t )
226+
208227 return self
209228
210229 def stop (self , force : bool = True , delete_volume : bool = True ) -> None :
@@ -270,6 +289,16 @@ def with_volume_mapping(self, host: Union[str, PathLike[str]], container: str, m
270289 self .volumes [str (host )] = mapping
271290 return self
272291
292+ def with_tmpfs_mount (self , container_path : str , size : Optional [str ] = None ) -> Self :
293+ """Mount a tmpfs volume on the container.
294+
295+ :param container_path: Container path to mount tmpfs on (e.g., '/data')
296+ :param size: Optional size limit (e.g., '256m', '1g'). If None, unbounded.
297+ :return: Self for chaining
298+ """
299+ self .tmpfs [container_path ] = size or ""
300+ return self
301+
273302 def get_wrapped_container (self ) -> "Container" :
274303 return self ._container
275304
@@ -301,10 +330,60 @@ def exec(self, command: Union[str, list[str]]) -> ExecResult:
301330 raise ContainerStartException ("Container should be started before executing a command" )
302331 return self ._container .exec_run (command )
303332
333+ def get_container_info (self ) -> Optional [ContainerInspectInfo ]:
334+ """Get container information via docker inspect (lazy loaded).
335+
336+ Returns:
337+ Container inspect information or None if container is not started.
338+ """
339+ if self ._cached_container_info is not None :
340+ return self ._cached_container_info
341+
342+ if not self ._container :
343+ return None
344+
345+ try :
346+ self ._cached_container_info = self .get_docker_client ().get_container_inspect_info (self ._container .id )
347+
348+ except Exception as e :
349+ logger .warning (f"Failed to get container info for { self ._container .id } : { e } " )
350+ self ._cached_container_info = None
351+
352+ return self ._cached_container_info
353+
304354 def _configure (self ) -> None :
305355 # placeholder if subclasses want to define this and use the default start method
306356 pass
307357
358+ def with_copy_into_container (
359+ self , transferable : Transferable , destination_in_container : str , mode : int = 0o644
360+ ) -> Self :
361+ self ._transferable_specs .append ((transferable , destination_in_container , mode ))
362+ return self
363+
364+ def copy_into_container (self , transferable : Transferable , destination_in_container : str , mode : int = 0o644 ) -> None :
365+ return self ._transfer_into_container (transferable , destination_in_container , mode )
366+
367+ def _transfer_into_container (self , transferable : Transferable , destination_in_container : str , mode : int ) -> None :
368+ if not self ._container :
369+ raise ContainerStartException ("Container must be started before transferring files" )
370+
371+ data = build_transfer_tar (transferable , destination_in_container , mode )
372+ if not self ._container .put_archive (path = "/" , data = data ):
373+ raise OSError (f"Failed to put archive into container at { destination_in_container } " )
374+
375+ def copy_from_container (self , source_in_container : str , destination_on_host : pathlib .Path ) -> None :
376+ if not self ._container :
377+ raise ContainerStartException ("Container must be started before copying files" )
378+
379+ tar_stream , _ = self ._container .get_archive (source_in_container )
380+
381+ with tarfile .open (fileobj = io .BytesIO (b"" .join (tar_stream ))) as tar :
382+ for member in tar .getmembers ():
383+ extracted = tar .extractfile (member )
384+ if extracted is not None :
385+ destination_on_host .write_bytes (extracted .read ())
386+
308387
309388class Reaper :
310389 """
0 commit comments