docker-devkit 0.3.0.dev113__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.
@@ -14,6 +14,7 @@ import typer
14
14
  from bashrun.bash import bash, bash_output
15
15
  from build_artifact_registry.builds import push_build
16
16
  from build_artifact_registry.setup_oras import install_oras
17
+ from .config import MirrorConfig
17
18
  from .detect_gpu import GPU_TYPES, Gpu, detect_gpu
18
19
  from .documents import BakeDocument, digest_pinned, parse_bake
19
20
  from pydantic_settings import BaseSettings
@@ -21,7 +22,7 @@ from .context_sha import compute_service_shas
21
22
 
22
23
  from .image_refs import bake_base_image_refs, resolve_remote_digest
23
24
  from .lifecycle import require_manifest, resolve_lock
24
- from .mirror import is_mirrored, load_mirror_config, upstream_ref
25
+ from .mirror import load_mirror_config, mirrored_ref
25
26
  from .modes import parse_env_file
26
27
 
27
28
 
@@ -85,11 +86,10 @@ def run_build(
85
86
  lock_only: bool = False,
86
87
  gpu_only: bool = False,
87
88
  no_cache: bool = False,
88
- env_overlay: dict[str, str] | None = None,
89
89
  ) -> None:
90
90
  root = Path.cwd()
91
91
  manifest = require_manifest(root)
92
- lock_file = resolve_lock(root, manifest)
92
+ lock_file = resolve_lock(root)
93
93
  mirror_config = load_mirror_config(root)
94
94
 
95
95
  bake = parse_bake(manifest)
@@ -117,24 +117,7 @@ def run_build(
117
117
  if gpu == "auto" and not lock_only:
118
118
  gpu = detect_gpu()
119
119
 
120
- # One lock entry per declared third-party image: NAME=path:tag@sha256:…
121
- # Mirror-prefixed declarations resolve their digest against upstream (crane copies
122
- # are byte-identical, so the digest is valid under both names) and record it under
123
- # the mirror name — declaring and re-locking works before CI has mirrored anything.
124
- for occurrence in bake_base_image_refs(bake):
125
- if digest_pinned(occurrence.reference):
126
- lock_data[occurrence.name] = occurrence.reference
127
- continue
128
- if (
129
- not upgrade
130
- and occurrence.name in lock_data
131
- and lock_data[occurrence.name].startswith(occurrence.reference + "@")
132
- ):
133
- continue
134
- inspect_target = occurrence.reference
135
- if mirror_config is not None and is_mirrored(occurrence.reference, mirror_config.prefix):
136
- inspect_target = upstream_ref(occurrence.reference, mirror_config.prefix)
137
- lock_data[occurrence.name] = f"{occurrence.reference}@{resolve_remote_digest(inspect_target)}"
120
+ update_lock_entries(bake, mirror_config, lock_data, upgrade=upgrade)
138
121
 
139
122
  # Update main lock file
140
123
  lock_file.write_text(
@@ -147,8 +130,6 @@ def run_build(
147
130
 
148
131
  # Update environment with external dependency image digests
149
132
  os.environ.update(lock_data)
150
- if env_overlay:
151
- os.environ.update(env_overlay)
152
133
 
153
134
  # Build command arguments
154
135
  command_arguments: list[str] = []
@@ -226,6 +207,31 @@ def run_build(
226
207
  push_image_digests(shelf, sha, baked_images, targets)
227
208
 
228
209
 
210
+ # One lock entry per declared third-party image: NAME={prefix}/{ref}@sha256:… — the
211
+ # mirror-prefixed form is derived from the mirror prefix, the digest resolves against
212
+ # the upstream reference (crane copies are byte-identical, so the digest is valid
213
+ # under both names) and records under the mirror name.
214
+ def update_lock_entries(
215
+ bake: BakeDocument,
216
+ mirror_config: MirrorConfig | None,
217
+ lock_data: dict[str, str],
218
+ *,
219
+ upgrade: bool,
220
+ ) -> None:
221
+ if not bake.base_images:
222
+ return
223
+ if mirror_config is None:
224
+ raise RuntimeError("x-base-images declared without a mirror section in docker-devkit.yaml")
225
+ for occurrence in bake_base_image_refs(bake):
226
+ lock_base = mirrored_ref(occurrence.reference, mirror_config.prefix)
227
+ if digest_pinned(occurrence.reference):
228
+ lock_data[occurrence.name] = lock_base
229
+ continue
230
+ if not upgrade and occurrence.name in lock_data and lock_data[occurrence.name].startswith(lock_base + "@"):
231
+ continue
232
+ lock_data[occurrence.name] = f"{lock_base}@{resolve_remote_digest(occurrence.reference)}"
233
+
234
+
229
235
  def leg_key(targets: list[str]) -> str:
230
236
  return hashlib.sha256(",".join(sorted(targets)).encode("utf-8")).hexdigest()[:8]
231
237
 
docker_devkit/down.py CHANGED
@@ -31,7 +31,7 @@ def down(
31
31
  config = load_lifecycle_config(root)
32
32
  enforce_bake_declaration(root, config)
33
33
  manifest = resolve_manifest(root)
34
- lock_file = resolve_lock(root, manifest)
34
+ lock_file = resolve_lock(root)
35
35
 
36
36
  if not ENV_FILE.exists():
37
37
  raise RuntimeError("No .env file found")
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime
3
+ import datetime as dt
4
4
  import re
5
5
  import shlex
6
6
  import tomllib
@@ -12,6 +12,7 @@ from bashrun.bash import bash_output
12
12
  from pydantic import RootModel
13
13
 
14
14
  from .documents import BakeDocument, parse_bake
15
+ from .lifecycle import MANIFEST_PATH
15
16
 
16
17
  BUILD_ARG_PATTERN = re.compile(r"\$\{[A-Za-z0-9_]+\}")
17
18
  FROM_PATTERN = re.compile(r"^FROM\s+(?:--platform=\S+\s+)?(\S+)", re.MULTILINE)
@@ -21,17 +22,7 @@ DIGEST_PATTERN = re.compile(r"^Digest:\s+(sha256:[a-f0-9]+)", re.MULTILINE)
21
22
  MISSING_VERSION = "<missing>"
22
23
 
23
24
 
24
- type TomlValue = (
25
- str
26
- | int
27
- | float
28
- | bool
29
- | datetime.datetime
30
- | datetime.date
31
- | datetime.time
32
- | list[TomlValue]
33
- | dict[str, TomlValue]
34
- )
25
+ type TomlValue = str | int | float | bool | dt.datetime | dt.date | dt.time | list[TomlValue] | dict[str, TomlValue]
35
26
 
36
27
 
37
28
  class TomlDocument(RootModel[TomlValue]):
@@ -70,15 +61,11 @@ def unpinned_references(root: Path) -> list[str]:
70
61
  ]
71
62
 
72
63
 
73
- def declared_references(
74
- root: Path, bake_globs: tuple[str, ...] = ("workloads/images.yml", "compose*.bake.yml")
75
- ) -> list[ImageReference]:
76
- return [
77
- reference
78
- for bake_glob in bake_globs
79
- for path in sorted(root.glob(bake_glob))
80
- for reference in bake_base_image_refs(parse_bake(path))
81
- ]
64
+ def declared_references(root: Path) -> list[ImageReference]:
65
+ manifest = root / MANIFEST_PATH
66
+ if not manifest.exists():
67
+ return []
68
+ return bake_base_image_refs(parse_bake(manifest))
82
69
 
83
70
 
84
71
  def stray_references(
@@ -8,11 +8,9 @@ from .detect_gpu import Gpu
8
8
  DEFAULT_COMPOSE_FILE = Path("compose.yml")
9
9
  GPU_PLACEHOLDER = "{gpu}"
10
10
 
11
- # The image manifest and its lock are a named pair living in the same directory. The
12
- # legacy names keep resolving so a repo can upgrade the devkit before moving its files.
13
- MANIFEST_CANDIDATES = (Path("workloads/images.yml"), Path("compose.bake.yml"))
14
- MANIFEST_LOCK_PAIR = {"images.yml": "images.lock", "compose.bake.yml": ".env.lock"}
15
- LOCK_CANDIDATES = (Path("workloads/images.lock"), Path(".env.lock"))
11
+ # The image manifest and its lock are a named pair living in the same directory.
12
+ MANIFEST_PATH = Path("workloads/images.yml")
13
+ LOCK_PATH = Path("workloads/images.lock")
16
14
 
17
15
 
18
16
  def load_lifecycle_config(root: Path) -> LifecycleConfig | None:
@@ -21,27 +19,25 @@ def load_lifecycle_config(root: Path) -> LifecycleConfig | None:
21
19
 
22
20
 
23
21
  def resolve_manifest(root: Path) -> Path | None:
24
- return next((root / candidate for candidate in MANIFEST_CANDIDATES if (root / candidate).exists()), None)
22
+ manifest = root / MANIFEST_PATH
23
+ return manifest if manifest.exists() else None
25
24
 
26
25
 
27
26
  def require_manifest(root: Path) -> Path:
28
27
  manifest = resolve_manifest(root)
29
28
  if manifest is None:
30
- raise RuntimeError(f"No image manifest found; {MANIFEST_CANDIDATES[0]} is the canonical name")
29
+ raise RuntimeError(f"No image manifest at {MANIFEST_PATH}")
31
30
  return manifest
32
31
 
33
32
 
34
- def resolve_lock(root: Path, manifest: Path | None) -> Path:
35
- if manifest is not None:
36
- return manifest.parent / MANIFEST_LOCK_PAIR[manifest.name]
37
- existing = next((root / candidate for candidate in LOCK_CANDIDATES if (root / candidate).exists()), None)
38
- return existing if existing is not None else root / LOCK_CANDIDATES[0]
33
+ def resolve_lock(root: Path) -> Path:
34
+ return root / LOCK_PATH
39
35
 
40
36
 
41
37
  def enforce_bake_declaration(root: Path, config: LifecycleConfig | None) -> None:
42
- if config is None and any((root / candidate).exists() for candidate in MANIFEST_CANDIDATES):
38
+ if config is None and (root / MANIFEST_PATH).exists():
43
39
  raise RuntimeError(
44
- "image manifest present without a lifecycle section in docker-devkit.yaml; declare the section or rename the file"
40
+ "image manifest present without a lifecycle section in docker-devkit.yaml; declare the section"
45
41
  )
46
42
 
47
43
 
docker_devkit/mirror.py CHANGED
@@ -2,14 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import shlex
5
- import tempfile
6
5
  from pathlib import Path
7
6
 
8
7
  import typer
9
8
  from bashrun.bash import bash, bash_check
10
9
 
11
10
  from .config import MirrorConfig, load_config
12
- from .image_refs import ImageReference, declared_references
11
+ from .image_refs import declared_references
13
12
 
14
13
  app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
15
14
 
@@ -23,7 +22,7 @@ def mirror() -> None:
23
22
  if config is None:
24
23
  raise RuntimeError("a mirror section with 'prefix' is required in docker-devkit.yaml to mirror")
25
24
 
26
- targets = mirror_targets(root, config.prefix)
25
+ targets = sorted(declared_references(root), key=lambda occurrence: occurrence.reference)
27
26
  print(f"Install crane {CRANE_VERSION}")
28
27
  sudo = "sudo " if os.geteuid() != 0 else ""
29
28
  archive = "go-containerregistry_Linux_x86_64.tar.gz"
@@ -31,9 +30,9 @@ def mirror() -> None:
31
30
  bash(f"{sudo}tar -xzf {archive} -C /usr/local/bin crane")
32
31
  Path(archive).unlink()
33
32
  for target in targets:
34
- upstream = upstream_ref(target.reference, config.prefix)
35
- print(f"Mirror {upstream} -> {target.reference}")
36
- bash(f"crane copy {upstream} {target.reference}")
33
+ destination = mirrored_ref(target.reference, config.prefix)
34
+ print(f"Mirror {target.reference} -> {destination}")
35
+ bash(f"crane copy {target.reference} {destination}")
37
36
  print(f"Mirrored images: {len(targets)}")
38
37
 
39
38
 
@@ -42,49 +41,27 @@ def load_mirror_config(root: Path) -> MirrorConfig | None:
42
41
  return config.mirror if config is not None else None
43
42
 
44
43
 
45
- def is_mirrored(reference: str, prefix: str) -> bool:
46
- return reference.startswith(f"{prefix}/")
47
-
48
-
49
- def upstream_ref(reference: str, prefix: str) -> str:
50
- return reference[len(prefix) + 1 :]
44
+ def mirrored_ref(reference: str, prefix: str) -> str:
45
+ if reference.startswith(f"{prefix}/"):
46
+ raise RuntimeError(
47
+ f"x-base-images values are upstream-native; {reference} already carries the mirror prefix {prefix}"
48
+ )
49
+ return f"{prefix}/{reference}"
51
50
 
52
51
 
53
52
  def reference_exists(reference: str) -> bool:
54
53
  return bash_check(f"docker buildx imagetools inspect {shlex.quote(reference)}")
55
54
 
56
55
 
57
- def mirror_targets(root: Path, prefix: str) -> list[ImageReference]:
58
- return sorted(
59
- (occurrence for occurrence in declared_references(root) if is_mirrored(occurrence.reference, prefix)),
60
- key=lambda occurrence: occurrence.reference,
61
- )
62
-
63
-
64
- def upstream_fallback(lock_data: dict[str, str], prefix: str, *, allow_upstream_fallback: bool) -> dict[str, str]:
65
- missing = {
66
- name: value for name, value in lock_data.items() if is_mirrored(value, prefix) and not reference_exists(value)
67
- }
68
- if not missing:
69
- return {}
70
- if not allow_upstream_fallback:
71
- value = next(iter(missing.values()))
56
+ def enforce_mirrored(lock_data: dict[str, str]) -> None:
57
+ for value in sorted(lock_data.values()):
58
+ if reference_exists(value):
59
+ continue
72
60
  reference, _, digest = value.rpartition("@")
73
61
  raise RuntimeError(
74
- f"Image {reference or value} (digest {digest or 'unknown'}) is not mirrored yet. "
75
- "If this is a new upstream dependency that has not been through CI, pass --allow-upstream-fallback "
76
- "to pull the same digest from upstream for this run."
62
+ f"Image {reference or value} (digest {digest or 'unknown'}) is not mirrored yet; "
63
+ "the CI mirror job populates it when the declaring change runs"
77
64
  )
78
- return {name: upstream_ref(value, prefix) for name, value in missing.items()}
79
-
80
-
81
- def write_fallback_overlay(substitutions: dict[str, str]) -> Path:
82
- overlay = Path(tempfile.gettempdir()) / f"docker-devkit-upstream-fallback-{os.getpid()}.env"
83
- overlay.write_text(
84
- "\n".join(f"{name}={value}" for name, value in sorted(substitutions.items())) + "\n",
85
- encoding="utf-8",
86
- )
87
- return overlay
88
65
 
89
66
 
90
67
  def main() -> None:
docker_devkit/up.py CHANGED
@@ -16,7 +16,7 @@ from .lifecycle import (
16
16
  resolve_lock,
17
17
  resolve_manifest,
18
18
  )
19
- from .mirror import load_mirror_config, upstream_fallback, write_fallback_overlay
19
+ from .mirror import enforce_mirrored, load_mirror_config
20
20
  from .modes import parse_env_file, resolve_auth_mode
21
21
 
22
22
  ENV_FILE = Path(".env")
@@ -42,19 +42,12 @@ def up(
42
42
  "--dev",
43
43
  help="Layer the declared dev overlay (compose.dev.yml shape) over the production stack for bind-mount/debug bring-up.",
44
44
  ),
45
- allow_upstream_fallback: Annotated[
46
- bool,
47
- typer.Option(
48
- "--allow-upstream-fallback",
49
- help="Pull unmirrored lock entries from upstream at the same digest instead of failing closed.",
50
- ),
51
- ] = False,
52
45
  ) -> None:
53
46
  root = Path.cwd()
54
47
  config = load_lifecycle_config(root)
55
48
  enforce_bake_declaration(root, config)
56
49
  manifest = resolve_manifest(root)
57
- lock_file = resolve_lock(root, manifest)
50
+ lock_file = resolve_lock(root)
58
51
 
59
52
  if dev and (config is None or config.dev_file is None):
60
53
  raise RuntimeError("--dev requires a dev_file in the lifecycle section of docker-devkit.yaml")
@@ -75,21 +68,12 @@ def up(
75
68
 
76
69
  auth_mode = resolve_auth_mode(ENV_FILE)
77
70
 
78
- substitutions: dict[str, str] = {}
79
- fallback_overlay: Path | None = None
80
71
  mirror_config = load_mirror_config(root)
81
72
  if mirror_config is not None:
82
- substitutions = upstream_fallback(
83
- parse_env_file(lock_file), mirror_config.prefix, allow_upstream_fallback=allow_upstream_fallback
84
- )
85
- if substitutions:
86
- for name, upstream in sorted(substitutions.items()):
87
- print(f"Falling back to upstream for {name}: {upstream}", flush=True)
88
- os.environ.update(substitutions)
89
- fallback_overlay = write_fallback_overlay(substitutions)
73
+ enforce_mirrored(parse_env_file(lock_file))
90
74
 
91
75
  if build:
92
- run_build(gpu=gpu, env_overlay=substitutions)
76
+ run_build(gpu=gpu)
93
77
 
94
78
  if manifest is not None:
95
79
  os.environ.update(compute_service_shas(root, parse_bake(manifest)))
@@ -101,8 +85,6 @@ def up(
101
85
  files_args = " ".join(f"-f {compose_file}" for compose_file in compose_files)
102
86
  profile_flag = "--profile keycloak " if auth_mode == "keycloak" else ""
103
87
  compose_args = f"{files_args} {profile_flag}--project-directory {root} --env-file .env --env-file {lock_file}"
104
- if fallback_overlay is not None:
105
- compose_args += f" --env-file {fallback_overlay}"
106
88
 
107
89
  up_command = f"docker compose {compose_args} up"
108
90
  if not build:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: docker-devkit
3
- Version: 0.3.0.dev113
3
+ Version: 0.4.0
4
4
  Summary: Docker-stack lifecycle commands (up/down/build) driven by a pyproject-declared compose assembly
5
5
  License-File: LICENSE
6
6
  License-File: NOTICE
@@ -0,0 +1,21 @@
1
+ docker_devkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ docker_devkit/build_docker.py,sha256=-oH0RPIDm1EZOh6UP2MMEDSX0G6W6zM4YgZSHw6_8u0,12978
3
+ docker_devkit/config.py,sha256=UMAS6yF1E9PIbsRs2TVig9XwSNCvQe5w9eTLUHOPKq0,3112
4
+ docker_devkit/context_sha.py,sha256=lhCxUcYM98xCdDum3hsFRFw5WYrjvZUSMbOu_7ZmQjE,3041
5
+ docker_devkit/detect_gpu.py,sha256=C4e3vixvSSJS6xQiMVWp2IdWSAgMr551KrBTuIrkTk4,456
6
+ docker_devkit/documents.py,sha256=c6jqbBNUKyJOltex-8u536_niWhEL7ScOab_MwZmM9k,2195
7
+ docker_devkit/down.py,sha256=WDlq_RBpP84E1-5sp7MtKEhRB9wBOz8Ib59RoviZ8uQ,2179
8
+ docker_devkit/image_refs.py,sha256=dyngpklq8mz4rGzb7gZGg3o3i9q5YO6_DRAC9fBNRfw,5148
9
+ docker_devkit/lifecycle.py,sha256=_2ZTn2xpjhDFzdVacA2oog2CvsDujaoc8oqnRnpHtC8,1631
10
+ docker_devkit/matrix.py,sha256=6NyGL4Ol849dYlcMRzAL2yjFoN6sGkGwVaaxcDm3bG4,4530
11
+ docker_devkit/mirror.py,sha256=AGL_oQUv68DKXjEFVdD-NQoLbC6w3DZJ7h-lzzvdKwM,2334
12
+ docker_devkit/modes.py,sha256=Lwhblzu5Kn7qhfy_QMqE28XuflSIJtCRBYNX7Iq8Qfo,1510
13
+ docker_devkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ docker_devkit/score_generate.py,sha256=psbQm-MWf9A5YP0ov0rLPUiuVAQbUWn6yfUmB-CXurU,9434
15
+ docker_devkit/up.py,sha256=9h5fNuEfUQbQozae8yrueiSfS_Be_ne0SuR1j6tadbU,3394
16
+ docker_devkit-0.4.0.dist-info/METADATA,sha256=_RO7Dz5QqwAHku7kcbtB9VvCvRID1hpXCrLzsrHAyCM,533
17
+ docker_devkit-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
18
+ docker_devkit-0.4.0.dist-info/entry_points.txt,sha256=6cAHqlMvuXI00uTJLf73MTqWnLd1-m5sDgJrSyHWv4U,244
19
+ docker_devkit-0.4.0.dist-info/licenses/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
20
+ docker_devkit-0.4.0.dist-info/licenses/NOTICE,sha256=jvglZza3GCdN7v4_TLuXUqX9C_uABlwAKUkcIeKwk_8,26
21
+ docker_devkit-0.4.0.dist-info/RECORD,,
@@ -1,21 +0,0 @@
1
- docker_devkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- docker_devkit/build_docker.py,sha256=DtvTUTGHdQAkpBVelGObWyYD5Ldc05iPWXkNpCMQt-E,12927
3
- docker_devkit/config.py,sha256=UMAS6yF1E9PIbsRs2TVig9XwSNCvQe5w9eTLUHOPKq0,3112
4
- docker_devkit/context_sha.py,sha256=lhCxUcYM98xCdDum3hsFRFw5WYrjvZUSMbOu_7ZmQjE,3041
5
- docker_devkit/detect_gpu.py,sha256=C4e3vixvSSJS6xQiMVWp2IdWSAgMr551KrBTuIrkTk4,456
6
- docker_devkit/documents.py,sha256=c6jqbBNUKyJOltex-8u536_niWhEL7ScOab_MwZmM9k,2195
7
- docker_devkit/down.py,sha256=9OMXPZQ3SIdaSELc46hzkqNpLSwOJ1athCD8Bmlz2BI,2189
8
- docker_devkit/image_refs.py,sha256=6pHYPWh4lL6qMqHtQBHBbeWijVsT0PhsMS6CMDho5nY,5294
9
- docker_devkit/lifecycle.py,sha256=FuwaCSRK7qVx_qj-HmIFqMm8Ak4cpAQEqqXRPDNCLmo,2259
10
- docker_devkit/matrix.py,sha256=6NyGL4Ol849dYlcMRzAL2yjFoN6sGkGwVaaxcDm3bG4,4530
11
- docker_devkit/mirror.py,sha256=tissqnoqMwRA6s0ykwXZOEgy6F1d3AAebPKGEkSpJRg,3224
12
- docker_devkit/modes.py,sha256=Lwhblzu5Kn7qhfy_QMqE28XuflSIJtCRBYNX7Iq8Qfo,1510
13
- docker_devkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- docker_devkit/score_generate.py,sha256=psbQm-MWf9A5YP0ov0rLPUiuVAQbUWn6yfUmB-CXurU,9434
15
- docker_devkit/up.py,sha256=32A2_6mxAF00XILecxYTzCQw0QvSjevBWCnayF5X1Xs,4284
16
- docker_devkit-0.3.0.dev113.dist-info/METADATA,sha256=0NT3OLGPZKW3hL5cvP-avLSORfSbo57cbDvwrZV2aOs,540
17
- docker_devkit-0.3.0.dev113.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
18
- docker_devkit-0.3.0.dev113.dist-info/entry_points.txt,sha256=6cAHqlMvuXI00uTJLf73MTqWnLd1-m5sDgJrSyHWv4U,244
19
- docker_devkit-0.3.0.dev113.dist-info/licenses/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
20
- docker_devkit-0.3.0.dev113.dist-info/licenses/NOTICE,sha256=jvglZza3GCdN7v4_TLuXUqX9C_uABlwAKUkcIeKwk_8,26
21
- docker_devkit-0.3.0.dev113.dist-info/RECORD,,