axio-tools-docker 0.9.0__tar.gz → 0.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: axio-tools-docker
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: Docker sandbox tools for Axio
5
5
  Project-URL: Homepage, https://github.com/mosquito/axio-agent
6
6
  Project-URL: Repository, https://github.com/mosquito/axio-agent
@@ -115,9 +115,10 @@ The running container's ID is available as `sandbox.container_id` inside the
115
115
 
116
116
  ## Named containers and reuse
117
117
 
118
- Pass `name=` to give the container a fixed name. If a running container with
119
- that name already exists, the sandbox attaches to it instead of creating a new
120
- one, and never removes it on exit - regardless of `remove`:
118
+ Pass `name=` to give the container a fixed name. If a container with that name
119
+ already exists, the sandbox starts it if needed and attaches instead of creating
120
+ a new one. Attached containers are never removed on exit - regardless of
121
+ `remove`:
121
122
 
122
123
  ```python
123
124
  import asyncio
@@ -101,9 +101,10 @@ The running container's ID is available as `sandbox.container_id` inside the
101
101
 
102
102
  ## Named containers and reuse
103
103
 
104
- Pass `name=` to give the container a fixed name. If a running container with
105
- that name already exists, the sandbox attaches to it instead of creating a new
106
- one, and never removes it on exit - regardless of `remove`:
104
+ Pass `name=` to give the container a fixed name. If a container with that name
105
+ already exists, the sandbox starts it if needed and attaches instead of creating
106
+ a new one. Attached containers are never removed on exit - regardless of
107
+ `remove`:
107
108
 
108
109
  ```python
109
110
  import asyncio
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "axio-tools-docker"
3
- version = "0.9.0"
3
+ version = "0.9.1"
4
4
  description = "Docker sandbox tools for Axio"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -24,6 +24,7 @@ testpaths = ["tests", "README.md"]
24
24
 
25
25
  [tool.ruff]
26
26
  line-length = 119
27
+ output-format = "concise"
27
28
  target-version = "py312"
28
29
 
29
30
  [tool.ruff.lint]
@@ -33,6 +34,10 @@ select = ["E", "F", "I", "UP"]
33
34
  strict = true
34
35
  python_version = "3.12"
35
36
 
37
+ [[tool.mypy.overrides]]
38
+ module = "aiodocker.*"
39
+ follow_imports = "skip"
40
+
36
41
  [dependency-groups]
37
42
  dev = [
38
43
  "pytest>=8",
@@ -43,4 +48,3 @@ dev = [
43
48
  "markdown-pytest>=0.6.0",
44
49
  "aiodocker>=0.26",
45
50
  ]
46
-
@@ -12,7 +12,7 @@ import stat as stat_module
12
12
  import tarfile
13
13
  import uuid
14
14
  from datetime import datetime
15
- from typing import Any
15
+ from typing import Any, cast
16
16
 
17
17
  import aiodocker
18
18
  from axio.tool import CONTEXT, Tool
@@ -324,6 +324,9 @@ class DockerSandbox:
324
324
  if self.name:
325
325
  try:
326
326
  self._container = await self._client.containers.get(self.name)
327
+ info = await self._container.show()
328
+ if not info.get("State", {}).get("Running", False):
329
+ await self._container.start()
327
330
  self._attached = True
328
331
  logger.info("Attached to existing container (name=%s)", self.name)
329
332
  except aiodocker.exceptions.DockerError:
@@ -420,7 +423,7 @@ class DockerSandbox:
420
423
  if self.named_volumes and self.volumes_remove and not was_attached:
421
424
  for vol_name in self.named_volumes.values():
422
425
  with contextlib.suppress(Exception):
423
- vol = await self._client.volumes.get(vol_name) # type: ignore[no-untyped-call]
426
+ vol = await self._client.volumes.get(vol_name)
424
427
  await vol.delete()
425
428
  logger.info("Removed %d named volume(s)", len(self.named_volumes))
426
429
  await self._client.close()
@@ -439,7 +442,7 @@ class DockerSandbox:
439
442
  """Return the ID of the running container. Only valid inside `async with`."""
440
443
  if self._container is None:
441
444
  raise RuntimeError("DockerSandbox must be used as an async context manager")
442
- return self._container.id
445
+ return str(self._container.id)
443
446
 
444
447
  async def _ensure_image(self) -> None:
445
448
  """Pull the image if it is not present locally."""
@@ -514,7 +517,7 @@ class DockerSandbox:
514
517
  tar.addfile(info, io.BytesIO(data))
515
518
  parent = os.path.dirname(path) or "/"
516
519
  await self.exec(f"mkdir -p {shlex.quote(parent)}")
517
- await self._container.put_archive( # type: ignore[no-untyped-call]
520
+ await self._container.put_archive(
518
521
  path=parent,
519
522
  data=buf.getvalue(),
520
523
  )
@@ -523,7 +526,7 @@ class DockerSandbox:
523
526
  async def get_archive(self, path: str) -> tarfile.TarFile:
524
527
  """Fetch a path from the container as a TarFile object."""
525
528
  assert self._container is not None
526
- return await self._container.get_archive(path=path)
529
+ return cast(tarfile.TarFile, await self._container.get_archive(path=path))
527
530
 
528
531
  async def read_file_bytes(self, path: str) -> bytes:
529
532
  """Read a file from inside the container and return raw bytes."""
@@ -360,7 +360,7 @@ async def test_named_volume_persists_across_containers(docker: str, image: str)
360
360
  finally:
361
361
  async with aiodocker.Docker(url=docker) as client:
362
362
  with contextlib.suppress(Exception):
363
- vol = await client.volumes.get(vol_name) # type: ignore[no-untyped-call]
363
+ vol = await client.volumes.get(vol_name)
364
364
  await vol.delete()
365
365
 
366
366
 
@@ -378,4 +378,4 @@ async def test_volumes_remove_cleans_up(docker: str, image: str) -> None:
378
378
 
379
379
  async with aiodocker.Docker(url=docker) as client:
380
380
  with pytest.raises(aiodocker.exceptions.DockerError):
381
- await client.volumes.get(vol_name) # type: ignore[no-untyped-call]
381
+ await client.volumes.get(vol_name)
@@ -63,6 +63,7 @@ def mock_docker_factory(
63
63
  mock_container.start = AsyncMock()
64
64
  mock_container.delete = AsyncMock()
65
65
  mock_container.exec = AsyncMock(return_value=mock_exec)
66
+ mock_container.show = AsyncMock(return_value={"State": {"Running": True}})
66
67
  mock_container.put_archive = AsyncMock()
67
68
  mock_container.get_archive = AsyncMock(
68
69
  return_value=archive_content if archive_content is not None else make_tar_file("file.txt", b"content")
@@ -166,6 +167,16 @@ async def test_named_existing_container_attaches() -> None:
166
167
  container.start.assert_not_awaited()
167
168
 
168
169
 
170
+ async def test_named_existing_stopped_container_starts_before_attach() -> None:
171
+ cls, client, container = mock_docker_factory()
172
+ client.containers.get = AsyncMock(return_value=container)
173
+ container.show = AsyncMock(return_value={"State": {"Running": False}})
174
+ with patch("axio_tools_docker.sandbox.aiodocker.Docker", cls):
175
+ async with DockerSandbox(name="my-sandbox"):
176
+ pass
177
+ container.start.assert_awaited_once()
178
+
179
+
169
180
  async def test_named_existing_container_not_deleted() -> None:
170
181
  """Attached container is never removed even with remove=True."""
171
182
  cls, client, container = mock_docker_factory()