├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── docker_mounter └── mounter.py ├── poetry.lock ├── pyproject.toml └── tests ├── test_docker_integration.py └── test_mounter.py /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.11", "3.12"] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install Poetry 25 | uses: snok/install-poetry@v1 26 | with: 27 | version: 1.7.1 28 | virtualenvs-create: true 29 | virtualenvs-in-project: true 30 | 31 | - name: Load cached venv 32 | id: cached-poetry-dependencies 33 | uses: actions/cache@v3 34 | with: 35 | path: .venv 36 | key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} 37 | 38 | - name: Install dependencies 39 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 40 | run: poetry install --no-interaction --no-root 41 | 42 | - name: Install project 43 | run: poetry install --no-interaction 44 | 45 | - name: Run tests 46 | run: poetry run pytest -v -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | 173 | # pytest cache 174 | .pytest_cache/ 175 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Joseph Redfern 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-mounter 2 | Utility to mount Docker images locally without requiring container creation. This is useful for analysing contents of 3 | Docker images from within the host operating system without incurring the overhead of container creation. 4 | 5 | This tool relies on some potentially unstable docker implementation details, and may break in future Docker versions! 6 | 7 | ## Installation 8 | 9 | This tool can be installed from PyPI using: 10 | 11 | ```bash 12 | pip install docker-mounter 13 | ``` 14 | 15 | Alternatively, the tool can be installed from source using: 16 | 17 | ```bash 18 | poetry install 19 | ``` 20 | 21 | ## Compatibility 22 | Due to use of overlay2, tool is only compatible with Linux 3.19+. 23 | 24 | It has currently only been tested with Docker version 27.3.1, build ce12230. 25 | 26 | ## Required Permissions 27 | This tool requires: 28 | 1. Access to the Docker daemon socket (user must be root or in the docker group) 29 | 2. Root privileges when using the `--mount` option 30 | 31 | ## Usage 32 | 33 | ```bash 34 | Usage: docker-mount [OPTIONS] IMAGE 35 | 36 | ╭─ Arguments ─────────────────────────────────────────────────────────╮ 37 | │ * image TEXT [default: None] [required] │ 38 | ╰─────────────────────────────────────────────────────────────────────╯ 39 | ╭─ Options ───────────────────────────────────────────────────────────╮ 40 | │ --mount-point PATH [default: None] │ 41 | │ --pull --no-pull [default: no-pull] │ 42 | │ --mount --no-mount [default: no-mount] │ 43 | │ --help Show this message and exit. │ 44 | ╰─────────────────────────────────────────────────────────────────────╯ 45 | ``` 46 | 47 | ## Example Usage 48 | 49 | ### Mount ubuntu:latest image to /mnt/docker-image and pull the image if it is not present 50 | ```bash 51 | sudo docker-mount --mount --pull --mount-point /mnt/docker-image ubuntu:latest 52 | ``` 53 | 54 | ### Mount ubuntu:latest image to /mnt/docker-image and do not pull the image 55 | ```bash 56 | sudo docker-mount --mount --no-pull --mount-point /mnt/docker-image ubuntu:latest 57 | ``` 58 | 59 | ### Generate command to mount ubuntu:latest image to /mnt/docker-image but do not mount or pull the image 60 | ```bash 61 | docker-mount --mount-point /mnt/docker-image ubuntu:latest 62 | ``` 63 | -------------------------------------------------------------------------------- /docker_mounter/mounter.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import subprocess 3 | import sys 4 | import tempfile 5 | from pathlib import Path 6 | 7 | import docker 8 | import typer 9 | from loguru import logger 10 | 11 | app = typer.Typer() 12 | 13 | """ 14 | Docker images are made up of multiple layers. Each layer contains some subset of the overall filesystem for the container. 15 | 16 | When the Docker image is pulled, Docker downloads a bunch of tar files (for each layer), and some metadata. The tar files 17 | are extracted to directories in `/var/lib/docker/overlay2` at pull time. When a container is started, `overlayfs` is used 18 | to mount a merged representation of the layers as a single coherent filesystem. You can find the path to the merged 19 | directory on the host OS by running: `docker inspect CONTAINER_NAME_HERE | jq '.[0].GraphDriver.Data.MergedDir'`, where 20 | `CONTAINER_NAME_HERE` is the name of a running container. 21 | 22 | There is no official way of mounting a Docker image without starting a container - but this is pretty inefficient. 23 | 24 | Alternatively, we can implement this ourselves. We can get a list of layers for an image (rather than a container) with: 25 | 26 | docker inspect hello-world:latest | jq .[0].RootFS.Layers 27 | 28 | this gives a list of sha256 hashes for each layer. These layers don't map directly to directories in 29 | `/var/lib/docker/overlay2` - instead a chain ID is used to name the directory. Chain ID is computed in 30 | `compute_chain_id` below. 31 | 32 | The computed chain IDs correspond to directories in: `/var/lib/docker/image/overlay2/layerdb/sha256/`. 33 | 34 | For instance, for `nginx:latest`, we get: 35 | 36 | [ 37 | "sha256:3e620c160447d1acff162610a533282fc64863123cba28ce40eaf98c17dde780", 38 | "sha256:880d2e736b16ec27cfe93230185b7fa9123b9b7007914ab06cad3dbcd03deaa0", 39 | "sha256:2c447934d7f2bbb627efecbd26692a9e28319d38d2936f7511bca77ffb7096de", 40 | "sha256:d06e03e55b64954a14f3de88bd37021e6c7c6d7d15aec93c6333e59ceb775f38", 41 | "sha256:d5c9fed2bbd4a673fc59864804e3f6a08cb72447eb5dc631c2f6903fbb089f57", 42 | "sha256:fc2efc334561650ca0f2be4e0245c176004739f50a5f965add8e6b417c227f03", 43 | "sha256:d93fefef05de8f71849a265e65bc5df15c67fbe7b14e51cac17794cb9f11ca1f" 44 | ] 45 | 46 | let's call this aray `layer_hashes`. 47 | 48 | The chain IDs here are: 49 | 50 | [ 51 | layer_hashes[0], 52 | sha256(layer_hashes[0] + " " + layer_hashes[1]), 53 | sha256(sha256(layer_hashes[0] + " " + layer_hashes[1]) + " " + layer_hashes[2]), 54 | sha256(sha256(sha256(layer_hashes[0] + " " + layer_hashes[1]) + " " + layer_hashes[2]) + " " + layer_hashes[3]), 55 | ... 56 | ] 57 | 58 | These chain IDs correspond to directories in `/var/lib/docker/images/overlay2/layerdb/sha256/`. Within those 59 | directories, a file called `cache-id` gives us the directory name in `/var/lib/docker/overlay2` for that layer. It's 60 | fairly convoluted but with this information we can manually construct the merged filesystem for any Docker image. 61 | 62 | (note the actual implementation uses ANOTHER layer of indirection, referencing hard links, but this is an 63 | implementation detail!) 64 | 65 | Some useful links: 66 | - https://earthly.dev/blog/docker-image-storage-on-host/ 67 | - https://askubuntu.com/a/704358 68 | - https://docs.docker.com/engine/storage/drivers/overlayfs-driver/ 69 | """ 70 | 71 | DOCKER_ROOT = Path("/var/lib/docker") 72 | 73 | 74 | class DockerMounterException(Exception): 75 | """ 76 | Base class for exceptions in this module. 77 | """ 78 | 79 | pass 80 | 81 | 82 | def compute_chain_id(diff_id: str, parent_chain_id: str = "") -> str: 83 | """ 84 | Compute a layer's ChainID according to Docker's specification. 85 | 86 | ChainID = SHA256(Parent's ChainID + " " + DiffID) 87 | For the base layer (no parent), ChainID = DiffID 88 | 89 | Args: 90 | diff_id: The layer's DiffID 91 | parent_chain_id: The parent layer's ChainID 92 | 93 | Returns: 94 | The layer's ChainID 95 | """ 96 | if not parent_chain_id: 97 | return diff_id 98 | 99 | # Docker concatenates with a spacem 100 | chain_string = f"{parent_chain_id} {diff_id}" 101 | return f"sha256:{hashlib.sha256(chain_string.encode()).hexdigest()}" 102 | 103 | 104 | def compute_chain_ids(diff_ids: list[str]) -> list[str]: 105 | """ 106 | Compute chain IDs for each layer in a Docker image. 107 | 108 | Args: 109 | diff_ids: List of layer diff IDs from the Docker image 110 | 111 | Returns: 112 | List of computed chain IDs corresponding to each layer 113 | """ 114 | chain_ids = [] 115 | parent_chain_id = "" 116 | for diff_id in diff_ids: 117 | chain_id = compute_chain_id(diff_id, parent_chain_id) 118 | chain_ids.append(chain_id) 119 | parent_chain_id = chain_id 120 | return chain_ids 121 | 122 | 123 | def generate_overlay_mount_command(mount_point: Path, lower_paths: list[Path], upper_dir: Path | None) -> str: 124 | """ 125 | Generate a mount command for overlayfs. 126 | """ 127 | 128 | if not mount_point.exists(): 129 | raise DockerMounterException(f"Mount point doesn't exist: {mount_point}") 130 | 131 | # If we're given an upper dir, use it. Otherwise, generate a temporary one. 132 | if upper_dir is None: 133 | upper_dir = Path(tempfile.mkdtemp()) 134 | logger.info(f"Using temporary upper dir: {upper_dir}") 135 | 136 | # Work dir is required by overlayfs for atomicity of operations... this will require manual cleanup 137 | work_dir = tempfile.mkdtemp() 138 | logger.info(f"Using temporary work dir: {work_dir}") 139 | upper_dir_arg = f",upperdir={upper_dir.resolve()},workdir={work_dir}" 140 | 141 | absolute_lower_paths = [str(path.resolve()) for path in lower_paths] 142 | 143 | return f"mount -t overlay -o lowerdir={':'.join(absolute_lower_paths)}{upper_dir_arg} none {mount_point.resolve()}" 144 | 145 | 146 | def get_hard_link_paths(chain_ids: list[str], diff_ids: list[str]) -> list[Path]: 147 | """ 148 | Docker saves space by using hard links for layers. This function returns the list of hard links. 149 | 150 | (note that diff_ids isn't strictly necessary here, but it's convenient to have for debugging) 151 | """ 152 | 153 | hard_links = [] 154 | 155 | for i, (chain_id, diff_id) in enumerate(zip(chain_ids, diff_ids)): 156 | chain_hash_type, chain_hash = chain_id.split(":") 157 | 158 | # Use ChainID to find the cache-id file. This cache-id is the name of the directory in 159 | # /var/lib/docker/overlay2 that contains the actual layer 160 | cache_id_path = DOCKER_ROOT / "image" / "overlay2" / "layerdb" / chain_hash_type / chain_hash / "cache-id" 161 | try: 162 | with cache_id_path.open() as f: 163 | cache_id = f.read().strip() 164 | if not cache_id: 165 | raise DockerMounterException(f"Empty cache-id file for layer {diff_id}") 166 | except FileNotFoundError: 167 | raise DockerMounterException(f"Can't find cache-id file at {cache_id_path} for layer {diff_id}") 168 | 169 | overlay_directory = DOCKER_ROOT / "overlay2" / cache_id 170 | if not overlay_directory.exists(): 171 | raise DockerMounterException(f"Overlay directory doesn't exist: {overlay_directory} for layer {diff_id}") 172 | 173 | hard_link_file = overlay_directory / "link" 174 | if not hard_link_file.exists(): 175 | raise DockerMounterException(f"Hard link file doesn't exist: {hard_link_file} for layer {diff_id}") 176 | else: 177 | with hard_link_file.open() as f: 178 | hard_link = f.read().strip() 179 | hard_links.append(hard_link) 180 | 181 | logger.debug(f"Found layer {i+1}:") 182 | logger.debug(f" ChainID: {chain_id}") 183 | logger.debug(f" DiffID: {diff_id}") 184 | logger.debug(f" Path: {overlay_directory}") 185 | logger.debug(f" Hard link: {hard_link}") 186 | 187 | # Get the absolute paths to each layer 188 | absolute_paths = [DOCKER_ROOT / "overlay2" / "l" / hard_link for hard_link in hard_links] 189 | 190 | return absolute_paths 191 | 192 | 193 | def resolve_and_generate_mount_command( 194 | image_name: str, 195 | mount_point: Path | None = None, 196 | upper_dir: Path | None = None, 197 | pull: bool = False, 198 | ) -> tuple[str, Path]: 199 | """ 200 | Generate command to mount a Docker image on the host OS. 201 | 202 | Args: 203 | image_name: The name of the Docker image to mount 204 | mount_point: The path to mount the image at 205 | upper_dir: The path to the upper directory for the mount 206 | 207 | Raises: 208 | DockerMounterException: If the mount point or image doesn't exist 209 | 210 | Returns: 211 | The mount command as a string 212 | """ 213 | 214 | client = docker.from_env() 215 | 216 | # Try to get the image from the local cache, optionally pull it if we don't have it 217 | try: 218 | image = client.images.get(image_name) 219 | logger.info(f"Using cached image: {image_name}") 220 | except docker.errors.ImageNotFound as e: 221 | if not pull: 222 | raise DockerMounterException(f"Image not found and pull is disabled: {image_name}") from e 223 | else: 224 | try: 225 | logger.info(f"Pulling image: {image_name}...") 226 | image = client.images.pull(image_name) 227 | logger.info(f"Pulled image: {image_name}") 228 | except ( 229 | docker.errors.APIError, 230 | docker.errors.ImageNotFound, 231 | ValueError, 232 | ) as e: 233 | raise DockerMounterException(f"Failed to pull image {image_name}") from e 234 | 235 | # Get the DiffIDs from the image metadata 236 | diff_ids = image.attrs.get("RootFS", {}).get("Layers", []) 237 | 238 | # Compute ChainIDs for each layer - these are used to find the actual layer directories 239 | chain_ids = compute_chain_ids(diff_ids) 240 | 241 | # Track the hard-links that Docker has generated for each layer (this follows the same pattern as Docker) 242 | hard_link_paths = get_hard_link_paths(chain_ids, diff_ids) 243 | 244 | logger.debug(f"Hard link paths: {[str(path) for path in hard_link_paths]}") 245 | 246 | if mount_point is None: 247 | mount_point = Path(tempfile.mkdtemp()) 248 | logger.info(f"Using temporary mount point: {mount_point}") 249 | 250 | mount_command = generate_overlay_mount_command(mount_point, hard_link_paths, upper_dir) 251 | 252 | return mount_command, mount_point 253 | 254 | 255 | @app.command() 256 | def main(image: str, mount_point: Path | None = None, pull: bool = False, mount: bool = False) -> None: 257 | try: 258 | mount_command, mount_point = resolve_and_generate_mount_command(image, mount_point, pull=pull) 259 | except DockerMounterException as e: 260 | raise typer.Exit(1) from e 261 | 262 | if mount: 263 | logger.info(f"Running mount command: {mount_command}") 264 | process = subprocess.run(mount_command, stdout=subprocess.PIPE, shell=True) 265 | if process.returncode != 0: 266 | raise typer.Exit(1) from DockerMounterException(f"Mount failed with return code {process.returncode}") 267 | else: 268 | logger.success(f"Mount successful for {image}, mounted at {str(mount_point)}") 269 | logger.info(f"You can unmount with: `umount {str(mount_point)}`") 270 | else: 271 | logger.info(f"Mount command: {mount_command}") 272 | logger.info(f"Mount point: {str(mount_point)}") 273 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "certifi" 5 | version = "2024.12.14" 6 | description = "Python package for providing Mozilla's CA Bundle." 7 | optional = false 8 | python-versions = ">=3.6" 9 | files = [ 10 | {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, 11 | {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, 12 | ] 13 | 14 | [[package]] 15 | name = "charset-normalizer" 16 | version = "3.4.1" 17 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 18 | optional = false 19 | python-versions = ">=3.7" 20 | files = [ 21 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, 22 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, 23 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, 24 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, 25 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, 26 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, 27 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, 28 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, 29 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, 30 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, 31 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, 32 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, 33 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, 34 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, 35 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, 36 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, 37 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, 38 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, 39 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, 40 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, 41 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, 42 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, 43 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, 44 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, 45 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, 46 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, 47 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, 48 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, 49 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, 50 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, 51 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, 52 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, 53 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, 54 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, 55 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, 56 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, 57 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, 58 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, 59 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, 60 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, 61 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, 62 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, 63 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, 64 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, 65 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, 66 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, 67 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, 68 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, 69 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, 70 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, 71 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, 72 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, 73 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, 74 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, 75 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, 76 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, 77 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, 78 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, 79 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, 80 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, 81 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, 82 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, 83 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, 84 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, 85 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, 86 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, 87 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, 88 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, 89 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, 90 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, 91 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, 92 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, 93 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, 94 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, 95 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, 96 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, 97 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, 98 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, 99 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, 100 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, 101 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, 102 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, 103 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, 104 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, 105 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, 106 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, 107 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, 108 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, 109 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, 110 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, 111 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, 112 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, 113 | ] 114 | 115 | [[package]] 116 | name = "click" 117 | version = "8.1.8" 118 | description = "Composable command line interface toolkit" 119 | optional = false 120 | python-versions = ">=3.7" 121 | files = [ 122 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 123 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 124 | ] 125 | 126 | [package.dependencies] 127 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 128 | 129 | [[package]] 130 | name = "colorama" 131 | version = "0.4.6" 132 | description = "Cross-platform colored terminal text." 133 | optional = false 134 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 135 | files = [ 136 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 137 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 138 | ] 139 | 140 | [[package]] 141 | name = "docker" 142 | version = "7.1.0" 143 | description = "A Python library for the Docker Engine API." 144 | optional = false 145 | python-versions = ">=3.8" 146 | files = [ 147 | {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, 148 | {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, 149 | ] 150 | 151 | [package.dependencies] 152 | pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} 153 | requests = ">=2.26.0" 154 | urllib3 = ">=1.26.0" 155 | 156 | [package.extras] 157 | dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"] 158 | docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] 159 | ssh = ["paramiko (>=2.4.3)"] 160 | websockets = ["websocket-client (>=1.3.0)"] 161 | 162 | [[package]] 163 | name = "idna" 164 | version = "3.10" 165 | description = "Internationalized Domain Names in Applications (IDNA)" 166 | optional = false 167 | python-versions = ">=3.6" 168 | files = [ 169 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 170 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 171 | ] 172 | 173 | [package.extras] 174 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 175 | 176 | [[package]] 177 | name = "iniconfig" 178 | version = "2.0.0" 179 | description = "brain-dead simple config-ini parsing" 180 | optional = false 181 | python-versions = ">=3.7" 182 | files = [ 183 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 184 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 185 | ] 186 | 187 | [[package]] 188 | name = "loguru" 189 | version = "0.7.3" 190 | description = "Python logging made (stupidly) simple" 191 | optional = false 192 | python-versions = "<4.0,>=3.5" 193 | files = [ 194 | {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, 195 | {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, 196 | ] 197 | 198 | [package.dependencies] 199 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 200 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 201 | 202 | [package.extras] 203 | dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] 204 | 205 | [[package]] 206 | name = "markdown-it-py" 207 | version = "3.0.0" 208 | description = "Python port of markdown-it. Markdown parsing, done right!" 209 | optional = false 210 | python-versions = ">=3.8" 211 | files = [ 212 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 213 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 214 | ] 215 | 216 | [package.dependencies] 217 | mdurl = ">=0.1,<1.0" 218 | 219 | [package.extras] 220 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 221 | code-style = ["pre-commit (>=3.0,<4.0)"] 222 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 223 | linkify = ["linkify-it-py (>=1,<3)"] 224 | plugins = ["mdit-py-plugins"] 225 | profiling = ["gprof2dot"] 226 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 227 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 228 | 229 | [[package]] 230 | name = "mdurl" 231 | version = "0.1.2" 232 | description = "Markdown URL utilities" 233 | optional = false 234 | python-versions = ">=3.7" 235 | files = [ 236 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 237 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 238 | ] 239 | 240 | [[package]] 241 | name = "packaging" 242 | version = "24.2" 243 | description = "Core utilities for Python packages" 244 | optional = false 245 | python-versions = ">=3.8" 246 | files = [ 247 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 248 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 249 | ] 250 | 251 | [[package]] 252 | name = "pluggy" 253 | version = "1.5.0" 254 | description = "plugin and hook calling mechanisms for python" 255 | optional = false 256 | python-versions = ">=3.8" 257 | files = [ 258 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 259 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 260 | ] 261 | 262 | [package.extras] 263 | dev = ["pre-commit", "tox"] 264 | testing = ["pytest", "pytest-benchmark"] 265 | 266 | [[package]] 267 | name = "pygments" 268 | version = "2.18.0" 269 | description = "Pygments is a syntax highlighting package written in Python." 270 | optional = false 271 | python-versions = ">=3.8" 272 | files = [ 273 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 274 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 275 | ] 276 | 277 | [package.extras] 278 | windows-terminal = ["colorama (>=0.4.6)"] 279 | 280 | [[package]] 281 | name = "pytest" 282 | version = "8.3.4" 283 | description = "pytest: simple powerful testing with Python" 284 | optional = false 285 | python-versions = ">=3.8" 286 | files = [ 287 | {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, 288 | {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, 289 | ] 290 | 291 | [package.dependencies] 292 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 293 | iniconfig = "*" 294 | packaging = "*" 295 | pluggy = ">=1.5,<2" 296 | 297 | [package.extras] 298 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 299 | 300 | [[package]] 301 | name = "pytest-mock" 302 | version = "3.14.0" 303 | description = "Thin-wrapper around the mock package for easier use with pytest" 304 | optional = false 305 | python-versions = ">=3.8" 306 | files = [ 307 | {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, 308 | {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, 309 | ] 310 | 311 | [package.dependencies] 312 | pytest = ">=6.2.5" 313 | 314 | [package.extras] 315 | dev = ["pre-commit", "pytest-asyncio", "tox"] 316 | 317 | [[package]] 318 | name = "pywin32" 319 | version = "308" 320 | description = "Python for Window Extensions" 321 | optional = false 322 | python-versions = "*" 323 | files = [ 324 | {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, 325 | {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, 326 | {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, 327 | {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, 328 | {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, 329 | {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, 330 | {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, 331 | {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, 332 | {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, 333 | {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, 334 | {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, 335 | {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, 336 | {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, 337 | {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, 338 | {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, 339 | {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, 340 | {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, 341 | {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, 342 | ] 343 | 344 | [[package]] 345 | name = "requests" 346 | version = "2.32.3" 347 | description = "Python HTTP for Humans." 348 | optional = false 349 | python-versions = ">=3.8" 350 | files = [ 351 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 352 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 353 | ] 354 | 355 | [package.dependencies] 356 | certifi = ">=2017.4.17" 357 | charset-normalizer = ">=2,<4" 358 | idna = ">=2.5,<4" 359 | urllib3 = ">=1.21.1,<3" 360 | 361 | [package.extras] 362 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 363 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 364 | 365 | [[package]] 366 | name = "rich" 367 | version = "13.9.4" 368 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 369 | optional = false 370 | python-versions = ">=3.8.0" 371 | files = [ 372 | {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, 373 | {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, 374 | ] 375 | 376 | [package.dependencies] 377 | markdown-it-py = ">=2.2.0" 378 | pygments = ">=2.13.0,<3.0.0" 379 | 380 | [package.extras] 381 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 382 | 383 | [[package]] 384 | name = "ruff" 385 | version = "0.8.4" 386 | description = "An extremely fast Python linter and code formatter, written in Rust." 387 | optional = false 388 | python-versions = ">=3.7" 389 | files = [ 390 | {file = "ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60"}, 391 | {file = "ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac"}, 392 | {file = "ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296"}, 393 | {file = "ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643"}, 394 | {file = "ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e"}, 395 | {file = "ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3"}, 396 | {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f"}, 397 | {file = "ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604"}, 398 | {file = "ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf"}, 399 | {file = "ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720"}, 400 | {file = "ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae"}, 401 | {file = "ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7"}, 402 | {file = "ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111"}, 403 | {file = "ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8"}, 404 | {file = "ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835"}, 405 | {file = "ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d"}, 406 | {file = "ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08"}, 407 | {file = "ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8"}, 408 | ] 409 | 410 | [[package]] 411 | name = "shellingham" 412 | version = "1.5.4" 413 | description = "Tool to Detect Surrounding Shell" 414 | optional = false 415 | python-versions = ">=3.7" 416 | files = [ 417 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 418 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 419 | ] 420 | 421 | [[package]] 422 | name = "typer" 423 | version = "0.15.1" 424 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 425 | optional = false 426 | python-versions = ">=3.7" 427 | files = [ 428 | {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"}, 429 | {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"}, 430 | ] 431 | 432 | [package.dependencies] 433 | click = ">=8.0.0" 434 | rich = ">=10.11.0" 435 | shellingham = ">=1.3.0" 436 | typing-extensions = ">=3.7.4.3" 437 | 438 | [[package]] 439 | name = "typing-extensions" 440 | version = "4.12.2" 441 | description = "Backported and Experimental Type Hints for Python 3.8+" 442 | optional = false 443 | python-versions = ">=3.8" 444 | files = [ 445 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 446 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 447 | ] 448 | 449 | [[package]] 450 | name = "urllib3" 451 | version = "2.3.0" 452 | description = "HTTP library with thread-safe connection pooling, file post, and more." 453 | optional = false 454 | python-versions = ">=3.9" 455 | files = [ 456 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 457 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 458 | ] 459 | 460 | [package.extras] 461 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 462 | h2 = ["h2 (>=4,<5)"] 463 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 464 | zstd = ["zstandard (>=0.18.0)"] 465 | 466 | [[package]] 467 | name = "win32-setctime" 468 | version = "1.2.0" 469 | description = "A small Python utility to set file creation time on Windows" 470 | optional = false 471 | python-versions = ">=3.5" 472 | files = [ 473 | {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, 474 | {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, 475 | ] 476 | 477 | [package.extras] 478 | dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] 479 | 480 | [metadata] 481 | lock-version = "2.0" 482 | python-versions = "^3.11" 483 | content-hash = "233ba1575c861e5197753dcde22d74e3f4ed65d382ee0fcd56b123d84846c9db" 484 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "docker-mounter" 3 | version = "0.1.0" 4 | description = "Utility to mount Docker images locally without requiring container creation" 5 | authors = ["Joseph Redfern "] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "docker_mounter"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.11" 12 | docker = "^7.1.0" 13 | loguru = "^0.7.3" 14 | typer = "^0.15.1" 15 | 16 | [tool.poetry.scripts] 17 | docker-mount = "docker_mounter.mounter:app" 18 | 19 | [tool.poetry.group.dev.dependencies] 20 | ruff = "^0.8.4" 21 | pytest = "^8.0.0" 22 | pytest-mock = "^3.12.0" 23 | 24 | [build-system] 25 | requires = ["poetry-core"] 26 | build-backend = "poetry.core.masonry.api" 27 | 28 | [tool.ruff] 29 | line-length = 120 30 | 31 | [tool.pytest.ini_options] 32 | testpaths = ["tests"] 33 | python_files = "test_*.py" -------------------------------------------------------------------------------- /tests/test_docker_integration.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pytest 3 | from pytest_mock import MockerFixture 4 | import docker 5 | from docker_mounter.mounter import resolve_and_generate_mount_command, DockerMounterException 6 | 7 | 8 | @pytest.fixture 9 | def mock_docker_client(mocker: MockerFixture): 10 | """Create a mock Docker client""" 11 | mock_client = mocker.patch("docker.from_env") 12 | return mock_client.return_value 13 | 14 | 15 | @pytest.fixture 16 | def mock_image(mocker: MockerFixture): 17 | """Create a mock Docker image""" 18 | mock = mocker.MagicMock() 19 | mock.attrs = { 20 | "RootFS": { 21 | "Layers": [ 22 | "sha256:layer1", 23 | "sha256:layer2", 24 | ] 25 | } 26 | } 27 | return mock 28 | 29 | 30 | def test_resolve_and_generate_mount_command_cached_image( 31 | mock_docker_client, mock_image, mocker: MockerFixture 32 | ): 33 | """Test resolving mount command for cached image""" 34 | mock_docker_client.images.get.return_value = mock_image 35 | 36 | # Mock the filesystem operations 37 | mocker.patch("pathlib.Path.exists", return_value=True) 38 | mocker.patch("pathlib.Path.open", mocker.mock_open(read_data="test-cache-id")) 39 | 40 | mount_point = Path("/tmp/test") 41 | command, result_mount_point = resolve_and_generate_mount_command( 42 | "test:latest", mount_point, pull=False 43 | ) 44 | 45 | assert isinstance(command, str) 46 | assert "mount -t overlay" in command 47 | assert result_mount_point == mount_point 48 | mock_docker_client.images.get.assert_called_once_with("test:latest") 49 | mock_docker_client.images.pull.assert_not_called() 50 | 51 | 52 | def test_resolve_and_generate_mount_command_pull_image( 53 | mock_docker_client, mock_image, mocker: MockerFixture 54 | ): 55 | """Test resolving mount command with image pull""" 56 | mock_docker_client.images.get.side_effect = [ 57 | docker.errors.ImageNotFound("Image not found"), 58 | mock_image, 59 | ] 60 | mock_docker_client.images.pull.return_value = mock_image 61 | 62 | # Mock the filesystem operations 63 | mocker.patch("pathlib.Path.exists", return_value=True) 64 | mocker.patch("pathlib.Path.open", mocker.mock_open(read_data="test-cache-id")) 65 | 66 | mount_point = Path("/tmp/test") 67 | command, result_mount_point = resolve_and_generate_mount_command( 68 | "test:latest", mount_point, pull=True 69 | ) 70 | 71 | assert isinstance(command, str) 72 | mock_docker_client.images.pull.assert_called_once_with("test:latest") 73 | 74 | 75 | def test_resolve_and_generate_mount_command_no_pull_failure(mock_docker_client): 76 | """Test failure when image not found and pull disabled""" 77 | mock_docker_client.images.get.side_effect = docker.errors.ImageNotFound("Image not found") 78 | 79 | with pytest.raises(DockerMounterException): 80 | resolve_and_generate_mount_command("test:latest", Path("/tmp/test"), pull=False) -------------------------------------------------------------------------------- /tests/test_mounter.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import pytest 3 | from docker_mounter.mounter import ( 4 | compute_chain_id, 5 | compute_chain_ids, 6 | DockerMounterException, 7 | generate_overlay_mount_command, 8 | ) 9 | 10 | 11 | def test_compute_chain_id_base_layer(): 12 | """Test computing chain ID for base layer (no parent)""" 13 | diff_id = "sha256:1234567890" 14 | result = compute_chain_id(diff_id) 15 | assert result == diff_id 16 | 17 | 18 | def test_compute_chain_id_with_parent(): 19 | """Test computing chain ID for layer with parent""" 20 | parent_chain_id = "sha256:1234567890" 21 | diff_id = "sha256:abcdef0123" 22 | result = compute_chain_id(diff_id, parent_chain_id) 23 | # This is the actual SHA256 hash of "sha256:1234567890 sha256:abcdef0123" 24 | expected = "sha256:83883872ef1b9b916151696045bcc621c9e0b12bb498636c7a42cfe29185f3aa" 25 | assert result == expected 26 | 27 | 28 | def test_compute_chain_ids(): 29 | """Test computing chain IDs for multiple layers""" 30 | diff_ids = [ 31 | "sha256:1234567890", 32 | "sha256:abcdef0123", 33 | "sha256:9876543210", 34 | ] 35 | result = compute_chain_ids(diff_ids) 36 | assert len(result) == 3 37 | assert result[0] == diff_ids[0] # First layer should be same as diff_id 38 | # Other layers should be computed based on their parent 39 | 40 | 41 | def test_generate_overlay_mount_command(mocker): 42 | """Test generating overlay mount command""" 43 | # Mock Path.exists to return True 44 | mocker.patch("pathlib.Path.exists", return_value=True) 45 | 46 | # Mock Path.resolve to return predictable paths 47 | def mock_resolve(self): 48 | return f"/resolved{str(self)}" 49 | mocker.patch.object(Path, "resolve", mock_resolve) 50 | 51 | mount_point = Path("/tmp/test") 52 | lower_paths = [Path("/lower1"), Path("/lower2")] 53 | upper_dir = Path("/upper") 54 | 55 | result = generate_overlay_mount_command(mount_point, lower_paths, upper_dir) 56 | 57 | assert "mount -t overlay" in result 58 | assert "lowerdir=/resolved/lower1:/resolved/lower2" in result 59 | assert "upperdir=/resolved/upper" in result 60 | assert "/resolved/tmp/test" in result 61 | 62 | 63 | def test_generate_overlay_mount_command_nonexistent_mount_point(mocker): 64 | """Test generating overlay mount command with nonexistent mount point""" 65 | # Mock Path.exists to return False for mount point 66 | mocker.patch("pathlib.Path.exists", return_value=False) 67 | 68 | mount_point = Path("/nonexistent") 69 | lower_paths = [Path("/lower1")] 70 | 71 | with pytest.raises(DockerMounterException): 72 | generate_overlay_mount_command(mount_point, lower_paths, None) --------------------------------------------------------------------------------