common-python-tasks 0.6.0__py3-none-any.whl → 0.8.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.
@@ -11,6 +11,15 @@ __all__ = ["TaskCollection"]
11
11
  def tasks(
12
12
  include_tags: "Sequence[str] | None" = None, exclude_tags: "Sequence[str]" = tuple()
13
13
  ) -> dict:
14
+ """Return the task collection filtered by the requested tags.
15
+
16
+ Args:
17
+ include_tags: Tags that selected tasks must have. Defaults to `common`.
18
+ exclude_tags: Tags that selected tasks must not have.
19
+
20
+ Returns:
21
+ The filtered Poe task collection configuration.
22
+ """
14
23
  from .tasks import tasks as task_collection
15
24
 
16
25
  if include_tags is None and not exclude_tags:
@@ -9,6 +9,19 @@ from .tasks import tasks
9
9
  __all__ = ["get_available_tasks", "print_available_tasks"]
10
10
 
11
11
 
12
+ def _is_package_task(task_name: str) -> bool:
13
+ task_config = tasks()["tasks"].get(task_name)
14
+ if not isinstance(task_config, dict):
15
+ return False
16
+
17
+ script = task_config.get("script")
18
+ if not isinstance(script, str) or ":" not in script:
19
+ return False
20
+
21
+ module_name, _ = script.split(":", 1)
22
+ return module_name.startswith("common_python_tasks.")
23
+
24
+
12
25
  def get_available_tasks(internal: bool = False) -> list[str]:
13
26
  """Return available task names for this package.
14
27
 
@@ -21,7 +34,7 @@ def get_available_tasks(internal: bool = False) -> list[str]:
21
34
  return [
22
35
  task_name
23
36
  for task_name in tasks()["tasks"]
24
- if internal or not task_name.startswith("_")
37
+ if _is_package_task(task_name) and (internal or not task_name.startswith("_"))
25
38
  ]
26
39
 
27
40
 
@@ -49,8 +62,11 @@ def _get_task_docstring(task_name: str) -> str | None:
49
62
 
50
63
  module_name, func_name = script.split(":", 1)
51
64
 
52
- module = importlib.import_module(module_name)
53
- func = getattr(module, func_name)
65
+ try:
66
+ module = importlib.import_module(module_name)
67
+ func = getattr(module, func_name)
68
+ except (ImportError, AttributeError):
69
+ return None
54
70
 
55
71
  doc = inspect.getdoc(func)
56
72
  if not doc:
@@ -5,16 +5,15 @@ prepend_sys_path = .
5
5
  output_encoding = utf-8
6
6
 
7
7
  [post_write_hooks]
8
- hooks = black,isort
8
+ hooks = ruff_check,ruff_format
9
9
 
10
- black.type = console_scripts
11
- black.entrypoint = black
10
+ ruff_check.type = exec
11
+ ruff_check.executable = ruff
12
+ ruff_check.options = check --fix-only --select F401,I REVISION_SCRIPT_FILENAME
12
13
 
13
- autoflake.type = console_scripts
14
- autoflake.entrypoint = autoflake
15
-
16
- isort.type = console_scripts
17
- isort.entrypoint = isort
14
+ ruff_format.type = exec
15
+ ruff_format.executable = ruff
16
+ ruff_format.options = format REVISION_SCRIPT_FILENAME
18
17
 
19
18
  # Logging configuration
20
19
  [loggers]
@@ -93,17 +93,17 @@ ENV {{ env_var }}
93
93
  {%- endif %}
94
94
 
95
95
  ENV DEBIAN_FRONTEND=noninteractive
96
- {% if APT_PACKAGES %}
96
+ {% if CONTAINER_APT_PACKAGES %}
97
97
  # Install optional runtime apt packages selected by Python-side template rendering
98
98
  RUN --mount=type=cache,target=/var/cache/apt,id=apt-cache{{ CACHE_ID_SUFFIX }} \
99
99
  --mount=type=cache,target=/var/lib/apt/lists,sharing=locked,id=apt-lists{{ CACHE_ID_SUFFIX }} \
100
- apt-get update && apt-get install -y --no-install-recommends {{ APT_PACKAGES }}
100
+ apt-get update && apt-get install -y --no-install-recommends {{ CONTAINER_APT_PACKAGES }}
101
101
  {% endif %}
102
- {% if DEPS_IMAGE %}
102
+ {% if CONTAINER_DEPS_IMAGE %}
103
103
 
104
104
  # Pull external dependencies from the pre-built deps image
105
- ARG DEPS_IMAGE
106
- COPY --from={{ DEPS_IMAGE }} /tmp/deps /tmp/deps
105
+ ARG CONTAINER_DEPS_IMAGE
106
+ COPY --from={{ CONTAINER_DEPS_IMAGE }} /tmp/deps /tmp/deps
107
107
  {% endif %}
108
108
  {% if CONTAINER_DEPS_MOVE_SCRIPT %}
109
109
  RUN set -eux; \
@@ -0,0 +1,2 @@
1
+ line-length = 88
2
+ extend-exclude = [".dist", ".poetry", "build"]
@@ -3,8 +3,10 @@ import logging
3
3
  import platform
4
4
  import re
5
5
  import tempfile
6
+ from dataclasses import dataclass
6
7
  from enum import IntEnum, StrEnum
7
8
  from pathlib import Path
9
+ from shlex import quote
8
10
  from typing import Sequence
9
11
 
10
12
  from . import git, project, utils
@@ -65,10 +67,7 @@ def _merge_build_args(
65
67
  return build_args
66
68
 
67
69
 
68
- def _create_temp_dockerfile(
69
- dockerfile_text: str,
70
- prefix: str,
71
- ) -> tuple[Path, str]:
70
+ def _create_temp_dockerfile(dockerfile_text: str, prefix: str) -> tuple[Path, str]:
72
71
  with tempfile.NamedTemporaryFile(
73
72
  mode="w",
74
73
  encoding="utf-8",
@@ -101,11 +100,11 @@ def _build_docker_build_command(
101
100
  plain: bool,
102
101
  target: BuildStage | None = None,
103
102
  tags: Sequence[str] | None = None,
103
+ docker_build_args: Sequence[str] | None = None,
104
104
  ) -> list[str]:
105
105
  build_cmd = [
106
106
  "docker",
107
107
  "build",
108
- str(context_path),
109
108
  "-f",
110
109
  str(dockerfile_path),
111
110
  ]
@@ -124,10 +123,267 @@ def _build_docker_build_command(
124
123
  build_cmd += ["-t", tag]
125
124
  if plain:
126
125
  build_cmd += ["--progress", "plain"]
126
+ if docker_build_args:
127
+ build_cmd += [str(arg) for arg in docker_build_args]
128
+ build_cmd.append(str(context_path))
127
129
 
128
130
  return build_cmd
129
131
 
130
132
 
133
+ def _run_dockerfile_hook(
134
+ hook_path: Path,
135
+ dockerfile_path: Path,
136
+ context_path: Path,
137
+ debug: bool,
138
+ no_cache: bool,
139
+ plain: bool,
140
+ single_arch: bool,
141
+ ) -> None:
142
+ utils.run_command(
143
+ [hook_path, dockerfile_path],
144
+ env={
145
+ "COMMON_PYTHON_TASKS_DOCKERFILE_PATH": str(dockerfile_path),
146
+ "COMMON_PYTHON_TASKS_DOCKER_CONTEXT": str(context_path),
147
+ "COMMON_PYTHON_TASKS_DOCKER_DEBUG": "1" if debug else "0",
148
+ "COMMON_PYTHON_TASKS_DOCKER_NO_CACHE": "1" if no_cache else "0",
149
+ "COMMON_PYTHON_TASKS_DOCKER_PLAIN": "1" if plain else "0",
150
+ "COMMON_PYTHON_TASKS_DOCKER_SINGLE_ARCH": "1" if single_arch else "0",
151
+ },
152
+ always_show_command=True,
153
+ )
154
+
155
+
156
+ @dataclass(frozen=True)
157
+ class RenderedBuildPlan:
158
+ """Canonical render of a Docker image build command and generated artifacts."""
159
+
160
+ dockerfile_text: str
161
+ dockerfile_path: Path
162
+ context_path: Path
163
+ build_args: dict[str, str]
164
+ tags: list[str]
165
+ version_tag: str
166
+ commit_tag: str
167
+ command: list[str]
168
+ command_display: str
169
+ target: BuildStage | None = None
170
+
171
+
172
+ def render_build_image(
173
+ dockerfile_path: Path | None = None,
174
+ dockerfile_text: str | None = None,
175
+ context_path: Path | None = None,
176
+ debug: bool = False,
177
+ no_cache: bool = False,
178
+ plain: bool = False,
179
+ single_arch: bool = False,
180
+ omit_target: bool = False,
181
+ image_name: str | None = None,
182
+ extra_build_args: dict[str, str] | None = None,
183
+ docker_build_args: Sequence[str] | None = None,
184
+ dockerfile_hook_path: Path | None = None,
185
+ keep_generated_files: bool = True,
186
+ ) -> RenderedBuildPlan:
187
+ """Render the canonical Docker build command without executing it.
188
+
189
+ Args:
190
+ dockerfile_path: Existing Dockerfile path to build from.
191
+ dockerfile_text: Dockerfile content to render into a temporary file.
192
+ context_path: Build context directory.
193
+ debug: Whether the debug image target should be used.
194
+ no_cache: Whether the Docker build should bypass cache.
195
+ plain: Whether Docker output should be plain text.
196
+ single_arch: Whether to limit the build to the current architecture.
197
+ omit_target: Whether to omit the target stage from the command.
198
+ image_name: Optional image name override.
199
+ extra_build_args: Extra build-time arguments.
200
+ docker_build_args: Additional arguments passed directly to `docker build`.
201
+ dockerfile_hook_path: Optional executable script path that can mutate
202
+ the generated Dockerfile in-place before build.
203
+ keep_generated_files: Whether to leave the generated Dockerfile in place.
204
+
205
+ Returns:
206
+ The rendered build plan that contains the command and generated artifacts.
207
+ """
208
+ dist_path = Path("dist")
209
+ if utils.directory_has_contents(dist_path):
210
+ utils.remove_path(dist_path)
211
+
212
+ context_path = _resolve_context_path(context_path)
213
+
214
+ temp_file_path = None
215
+ temp_dockerignore_created = False
216
+ dockerignore_path = context_path / ".dockerignore"
217
+ if dockerfile_path is None:
218
+ if dockerfile_text is None:
219
+ utils.fatal("Either dockerfile_path or dockerfile_text must be provided.")
220
+ dockerfile_path, temp_file_path = _create_temp_dockerfile(
221
+ dockerfile_text,
222
+ prefix="Dockerfile.",
223
+ )
224
+
225
+ if not dockerignore_path.exists():
226
+ LOGGER.debug("No .dockerignore found; using built-in .dockerignore")
227
+ builtin_dockerignore_content = utils.load_data_file(".dockerignore")[1]
228
+ dockerignore_path.write_text(builtin_dockerignore_content, encoding="utf-8")
229
+ temp_dockerignore_created = True
230
+
231
+ try:
232
+ if dockerfile_hook_path is not None:
233
+ if temp_file_path is None:
234
+ dockerfile_path, temp_file_path = _create_temp_dockerfile(
235
+ dockerfile_path.read_text(encoding="utf-8"),
236
+ prefix="Dockerfile.hooked.",
237
+ )
238
+ _run_dockerfile_hook(
239
+ dockerfile_hook_path,
240
+ dockerfile_path,
241
+ context_path,
242
+ debug,
243
+ no_cache,
244
+ plain,
245
+ single_arch,
246
+ )
247
+
248
+ archs = _resolve_archs(single_arch)
249
+ files_to_ignore = [Path(".dockerignore")] if temp_dockerignore_created else []
250
+ package_version = git.get_version(ignore=files_to_ignore)
251
+ version_string = git.get_image_tag(ignore=files_to_ignore)
252
+
253
+ dockerfile_source = dockerfile_path.read_text(encoding="utf-8")
254
+ if (
255
+ debug
256
+ and not omit_target
257
+ and not re.search(
258
+ rf"^\s*FROM\s+\S+\s+AS\s+{BuildStage.DEBUG}\s*$",
259
+ dockerfile_source,
260
+ re.IGNORECASE | re.MULTILINE,
261
+ )
262
+ ):
263
+ utils.fatal(
264
+ "Debug build requested, but the rendered Dockerfile has no debug stage. "
265
+ "Add a non-empty [dependency-groups].debug in pyproject.toml or build without debug."
266
+ )
267
+
268
+ if debug:
269
+ suffix = BuildSuffix.DEBUG
270
+ target = BuildStage.DEBUG
271
+ tag = BuildTag.DEBUG
272
+ else:
273
+ suffix = BuildSuffix.NONE
274
+ target = BuildStage.RUNTIME
275
+ tag = BuildTag.LATEST if not git.has_tags_later_in_history() else None
276
+
277
+ version_tag = f"{version_string}{suffix}"
278
+ if git.git_available():
279
+ commit_hash = utils.run_command(
280
+ ["git", "rev-parse", "--short", "HEAD"],
281
+ capture_output=True,
282
+ acceptable_returncodes={0},
283
+ ).stdout.strip()
284
+ dirty_suffix = (
285
+ "-dirty" if git.get_dirty_files(ignore=files_to_ignore) else ""
286
+ )
287
+ commit_tag = f"{commit_hash}{dirty_suffix}{suffix}"
288
+ else:
289
+ commit_tag = version_tag
290
+
291
+ python_version = platform.python_version()
292
+ package_manager = project.get_package_manager()
293
+ package_manager_version = project.get_package_manager_version(package_manager)
294
+
295
+ manager_build_args: dict[str, str | None]
296
+ if package_manager == project.PackageManager.POETRY:
297
+ manager_build_args = {
298
+ "POETRY_VERSION": package_manager_version,
299
+ "POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION": project.get_installed_requirement_version(
300
+ "poetry-dynamic-versioning[plugin]"
301
+ ),
302
+ "POETRY_PLUGIN_EXPORT_VERSION": project.get_installed_requirement_version(
303
+ "poetry-plugin-export"
304
+ ),
305
+ }
306
+ else:
307
+ manager_build_args = {
308
+ "UV_VERSION": package_manager_version,
309
+ }
310
+
311
+ build_args = _merge_build_args(
312
+ {
313
+ "PYTHON_VERSION": python_version,
314
+ "PACKAGE_MANAGER": package_manager,
315
+ "PACKAGE_MANAGER_VERSION": package_manager_version,
316
+ "PACKAGE_VERSION": package_version,
317
+ "PACKAGE_NAME": utils.get_package_name(use_underscores=True),
318
+ "AUTHORS": ",".join(
319
+ [f"{name} <{email}>" for name, email in project.get_authors()]
320
+ ),
321
+ "GIT_COMMIT": commit_tag,
322
+ **manager_build_args,
323
+ },
324
+ extra_build_args,
325
+ )
326
+
327
+ build_args.setdefault("WORKDIR_PATH", get_workdir_path())
328
+
329
+ tags_to_use = [t for t in (tag, version_tag, commit_tag) if t is not None]
330
+ LOGGER.debug("Building image with tags: %s", ", ".join(tags_to_use))
331
+ image_short_name = (
332
+ image_name if image_name is not None else utils.get_package_name()
333
+ )
334
+ orig_full_name = utils.get_full_image_name()
335
+
336
+ parsed_full_name = utils.parse_image_reference(orig_full_name)
337
+ short_tags = [
338
+ utils.compose_image_name(image_short_name, tag=t, fully_qualified=False)
339
+ for t in tags_to_use
340
+ ]
341
+ full_tags = [
342
+ utils.compose_image_name(
343
+ image_short_name,
344
+ tag=t,
345
+ registry=parsed_full_name["registry"],
346
+ namespace=parsed_full_name["namespace"],
347
+ )
348
+ for t in tags_to_use
349
+ ]
350
+
351
+ all_tags = []
352
+ for resolved_tag in [*short_tags, *full_tags]:
353
+ if resolved_tag not in all_tags:
354
+ all_tags.append(resolved_tag)
355
+
356
+ build_cmd = _build_docker_build_command(
357
+ context_path,
358
+ dockerfile_path,
359
+ build_args,
360
+ archs,
361
+ no_cache,
362
+ plain,
363
+ target=None if omit_target else target,
364
+ tags=all_tags,
365
+ docker_build_args=docker_build_args,
366
+ )
367
+ command_display = " ".join(quote(str(arg)) for arg in build_cmd)
368
+ return RenderedBuildPlan(
369
+ dockerfile_text=dockerfile_source,
370
+ dockerfile_path=dockerfile_path,
371
+ context_path=context_path,
372
+ build_args=build_args,
373
+ tags=all_tags,
374
+ version_tag=version_tag,
375
+ commit_tag=commit_tag,
376
+ command=build_cmd,
377
+ command_display=command_display,
378
+ target=None if omit_target else target,
379
+ )
380
+ finally:
381
+ if not keep_generated_files:
382
+ _safe_unlink(temp_file_path)
383
+ if temp_dockerignore_created:
384
+ _safe_unlink(dockerignore_path)
385
+
386
+
131
387
  def prune_images_keep(
132
388
  full_name: str,
133
389
  package_name: str,
@@ -241,6 +497,7 @@ def build_deps_image(
241
497
  plain: bool = False,
242
498
  single_arch: bool = False,
243
499
  extra_build_args: dict[str, str] | None = None,
500
+ docker_build_args: Sequence[str] | None = None,
244
501
  cache_id_suffix: str = "",
245
502
  ) -> str:
246
503
  """Build the dependency collector image and return its full tag.
@@ -253,6 +510,7 @@ def build_deps_image(
253
510
  plain: If `True`, use plain output mode for the Docker build.
254
511
  single_arch: If `True`, build the image for the current host architecture only.
255
512
  extra_build_args: Additional build arguments for the Docker build as a dictionary of `KEY: VALUE` pairs.
513
+ docker_build_args: Additional arguments passed directly to `docker build`.
256
514
  cache_id_suffix: A suffix to append to the cache ID.
257
515
 
258
516
  Returns:
@@ -337,6 +595,7 @@ def build_deps_image(
337
595
  no_cache,
338
596
  plain,
339
597
  tags=[full_tag],
598
+ docker_build_args=docker_build_args,
340
599
  )
341
600
 
342
601
  LOGGER.info("Building deps image: %s", full_tag)
@@ -359,6 +618,8 @@ def build_image(
359
618
  omit_target: bool = False,
360
619
  image_name: str | None = None,
361
620
  extra_build_args: dict[str, str] | None = None,
621
+ docker_build_args: Sequence[str] | None = None,
622
+ dockerfile_hook_path: Path | None = None,
362
623
  ) -> tuple[str, str]:
363
624
  """Build the primary image and return its version and commit tags.
364
625
 
@@ -373,165 +634,44 @@ def build_image(
373
634
  omit_target: If `True`, omit the target stage.
374
635
  image_name: Name of the image to build.
375
636
  extra_build_args: Additional build arguments for the Docker build as a dictionary of `KEY: VALUE` pairs.
637
+ docker_build_args: Additional arguments passed directly to `docker build`.
638
+ dockerfile_hook_path: Optional executable script path that can mutate
639
+ the generated Dockerfile in-place before build.
376
640
 
377
641
  Returns:
378
642
  A tuple containing the version tag and commit tag of the built image.
379
643
  """
380
- dist_path = Path("dist")
381
- if utils.directory_has_contents(dist_path):
382
- utils.remove_path(dist_path)
383
-
384
644
  context_path = _resolve_context_path(context_path)
385
-
386
- temp_file_path = None
387
- if dockerfile_path is None:
388
- if dockerfile_text is None:
389
- utils.fatal("Either dockerfile_path or dockerfile_text must be provided.")
390
- dockerfile_path, temp_file_path = _create_temp_dockerfile(
391
- dockerfile_text,
392
- prefix="Dockerfile.",
393
- )
394
-
395
645
  dockerignore_path = context_path / ".dockerignore"
396
- temp_dockerignore_created = False
397
- if not dockerignore_path.exists():
398
- LOGGER.debug("No .dockerignore found; using built-in .dockerignore")
399
- builtin_dockerignore_content = utils.load_data_file(".dockerignore")[1]
400
- dockerignore_path.write_text(builtin_dockerignore_content, encoding="utf-8")
401
- temp_dockerignore_created = True
646
+ dockerignore_existed = dockerignore_path.exists()
647
+ temp_dockerfile_created = dockerfile_path is None
402
648
 
403
- delete_temp_file = False
649
+ plan = render_build_image(
650
+ dockerfile_path=dockerfile_path,
651
+ dockerfile_text=dockerfile_text,
652
+ context_path=context_path,
653
+ debug=debug,
654
+ no_cache=no_cache,
655
+ plain=plain,
656
+ single_arch=single_arch,
657
+ omit_target=omit_target,
658
+ image_name=image_name,
659
+ extra_build_args=extra_build_args,
660
+ docker_build_args=docker_build_args,
661
+ dockerfile_hook_path=dockerfile_hook_path,
662
+ keep_generated_files=True,
663
+ )
404
664
  try:
405
- archs = _resolve_archs(single_arch)
406
- files_to_ignore = [Path(".dockerignore")] if temp_dockerignore_created else []
407
- package_version = git.get_version(ignore=files_to_ignore)
408
- version_string = git.get_image_tag(ignore=files_to_ignore)
409
-
410
- dockerfile_source = (
411
- dockerfile_text
412
- if dockerfile_text is not None
413
- else dockerfile_path.read_text(encoding="utf-8")
414
- )
415
- if (
416
- debug
417
- and not omit_target
418
- and not re.search(
419
- rf"^\s*FROM\s+\S+\s+AS\s+{BuildStage.DEBUG}\s*$",
420
- dockerfile_source,
421
- re.IGNORECASE | re.MULTILINE,
422
- )
423
- ):
424
- utils.fatal(
425
- "Debug build requested, but the rendered Dockerfile has no debug stage. "
426
- "Add a non-empty [dependency-groups].debug in pyproject.toml or build without debug."
427
- )
428
-
429
- if debug:
430
- suffix = BuildSuffix.DEBUG
431
- target = BuildStage.DEBUG
432
- tag = BuildTag.DEBUG
433
- else:
434
- suffix = BuildSuffix.NONE
435
- target = BuildStage.RUNTIME
436
- tag = BuildTag.LATEST if not git.has_tags_later_in_history() else None
437
-
438
- version_tag = f"{version_string}{suffix}"
439
- if git.git_available():
440
- commit_hash = utils.run_command(
441
- ["git", "rev-parse", "--short", "HEAD"],
442
- capture_output=True,
443
- acceptable_returncodes={0},
444
- ).stdout.strip()
445
- dirty_suffix = (
446
- "-dirty" if git.get_dirty_files(ignore=files_to_ignore) else ""
447
- )
448
- commit_tag = f"{commit_hash}{dirty_suffix}{suffix}"
449
- else:
450
- commit_tag = version_tag
451
-
452
- python_version = platform.python_version()
453
- package_manager = project.get_package_manager()
454
- package_manager_version = project.get_package_manager_version(package_manager)
455
-
456
- manager_build_args: dict[str, str | None]
457
- if package_manager == project.PackageManager.POETRY:
458
- manager_build_args = {
459
- "POETRY_VERSION": package_manager_version,
460
- "POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION": project.get_installed_requirement_version(
461
- "poetry-dynamic-versioning[plugin]"
462
- ),
463
- "POETRY_PLUGIN_EXPORT_VERSION": project.get_installed_requirement_version(
464
- "poetry-plugin-export"
465
- ),
466
- }
467
- else:
468
- manager_build_args = {
469
- "UV_VERSION": package_manager_version,
470
- }
471
-
472
- build_args = _merge_build_args(
473
- {
474
- "PYTHON_VERSION": python_version,
475
- "PACKAGE_MANAGER": package_manager,
476
- "PACKAGE_MANAGER_VERSION": package_manager_version,
477
- "PACKAGE_VERSION": package_version,
478
- "PACKAGE_NAME": utils.get_package_name(use_underscores=True),
479
- "AUTHORS": ",".join(
480
- [f"{name} <{email}>" for name, email in project.get_authors()]
481
- ),
482
- "GIT_COMMIT": commit_tag,
483
- **manager_build_args,
484
- },
485
- extra_build_args,
486
- )
487
-
488
- build_args.setdefault("WORKDIR_PATH", get_workdir_path())
489
-
490
- tags_to_use = [t for t in (tag, version_tag, commit_tag) if t is not None]
491
- LOGGER.debug("Building image with tags: %s", ", ".join(tags_to_use))
492
- image_short_name = (
493
- image_name if image_name is not None else utils.get_package_name()
494
- )
495
- orig_full_name = utils.get_full_image_name()
496
-
497
- parsed_full_name = utils.parse_image_reference(orig_full_name)
498
- short_tags = [
499
- utils.compose_image_name(image_short_name, tag=t, fully_qualified=False)
500
- for t in tags_to_use
501
- ]
502
- full_tags = [
503
- utils.compose_image_name(
504
- image_short_name,
505
- tag=t,
506
- registry=parsed_full_name["registry"],
507
- namespace=parsed_full_name["namespace"],
508
- )
509
- for t in tags_to_use
510
- ]
511
-
512
- all_tags = []
513
- for resolved_tag in [*short_tags, *full_tags]:
514
- if resolved_tag not in all_tags:
515
- all_tags.append(resolved_tag)
516
-
517
- build_cmd = _build_docker_build_command(
518
- context_path,
519
- dockerfile_path,
520
- build_args,
521
- archs,
522
- no_cache,
523
- plain,
524
- target=None if omit_target else target,
525
- tags=all_tags,
526
- )
527
- utils.run_command(build_cmd)
528
- delete_temp_file = True
665
+ utils.run_command(plan.command)
529
666
  finally:
530
- if temp_file_path is not None and delete_temp_file:
531
- _safe_unlink(dockerfile_path)
532
- if temp_dockerignore_created:
667
+ if temp_dockerfile_created or (
668
+ dockerfile_path is not None and plan.dockerfile_path != dockerfile_path
669
+ ):
670
+ _safe_unlink(plan.dockerfile_path)
671
+ if not dockerignore_existed:
533
672
  _safe_unlink(dockerignore_path)
534
- return version_tag, commit_tag
673
+
674
+ return plan.version_tag, plan.commit_tag
535
675
 
536
676
 
537
677
  def build_extension_image(
@@ -35,6 +35,13 @@ def ensure_alembic_config(compose_type: str) -> tuple[Path | None, bool]:
35
35
  LOGGER.debug("Using existing alembic.ini")
36
36
  return alembic_ini_path, False
37
37
 
38
+ generated_alembic_ini_path = Path(
39
+ f".{compose_type}.alembic.ini.common-python-tasks"
40
+ )
41
+ if generated_alembic_ini_path.exists():
42
+ LOGGER.debug("Using generated alembic.ini at %s", generated_alembic_ini_path)
43
+ return generated_alembic_ini_path, True
44
+
38
45
  result = utils.load_data_file(
39
46
  "alembic.ini.j2", type_identifier=compose_type, fatal_on_missing=False
40
47
  )
@@ -45,9 +52,9 @@ def ensure_alembic_config(compose_type: str) -> tuple[Path | None, bool]:
45
52
  rendered = Template(template_content).render(
46
53
  package_name=utils.get_package_name(use_underscores=True),
47
54
  )
48
- alembic_ini_path.write_text(rendered, encoding="utf-8")
49
- LOGGER.debug("Rendered bundled alembic.ini.j2 to %s", alembic_ini_path)
50
- return alembic_ini_path, True
55
+ generated_alembic_ini_path.write_text(rendered, encoding="utf-8")
56
+ LOGGER.debug("Rendered bundled alembic.ini.j2 to %s", generated_alembic_ini_path)
57
+ return generated_alembic_ini_path, True
51
58
 
52
59
 
53
60
  def render_file(
@@ -217,7 +224,7 @@ def get_or_generate_secret(
217
224
 
218
225
 
219
226
  def ensure_secrets_generated() -> None:
220
- """Ensure required secrets exist (generate once and persist to `.env`)."""
227
+ """Ensure required secrets exist and persist them to `.env`."""
221
228
  get_or_generate_secret("SECRET_KEY")
222
229
  get_or_generate_secret("DB_PASS")
223
230
 
@@ -514,6 +521,7 @@ def run_docker_compose_command(
514
521
  *args: Command arguments (None values are filtered out).
515
522
  compose_files: List of compose file paths.
516
523
  compose_env: Environment variables for the command.
524
+ tasks: Poe task collection that supplies configured env files.
517
525
  """
518
526
 
519
527
  utils.run_command(