common-python-tasks 0.10.3__py3-none-any.whl → 0.11.1__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.
@@ -3,6 +3,7 @@ import os
3
3
  import re
4
4
  import secrets
5
5
  import shlex
6
+ from collections.abc import Iterator
6
7
  from pathlib import Path
7
8
 
8
9
  from . import utils
@@ -53,51 +54,47 @@ def collect_uv_index_credentials() -> list[dict[str, str | None]]:
53
54
  ]
54
55
 
55
56
 
57
+ def _iter_uv_index_secrets(
58
+ credentials: list[dict[str, str | None]],
59
+ ) -> Iterator[tuple[str, str]]:
60
+ for credential in credentials:
61
+ index_name = credential["index_name"].lower()
62
+ for credential_type in ("username", "password"):
63
+ environment_name = credential[f"{credential_type}_env"]
64
+ if environment_name:
65
+ yield f"uv_index_{index_name}_{credential_type}", environment_name
66
+
67
+
56
68
  def uv_index_secret_mounts(credentials: list[dict[str, str | None]]) -> list[str]:
57
- """Return Dockerfile ``--mount`` strings for UV index credentials.
69
+ """Return Dockerfile `--mount` strings for UV index credentials.
58
70
 
59
71
  Args:
60
72
  credentials: Output of `collect_uv_index_credentials`.
61
73
 
62
74
  Returns:
63
- A list of ``type=secret,...`` strings for Dockerfile ``RUN --mount=``
75
+ A list of `type=secret,...` strings for Dockerfile `RUN --mount=`
64
76
  directives.
65
77
  """
66
- mounts = []
67
- for cred in credentials:
68
- name = cred["index_name"].lower()
69
- if cred["username_env"]:
70
- mounts.append(
71
- f"type=secret,id=uv_index_{name}_username,env={cred['username_env']}"
72
- )
73
- if cred["password_env"]:
74
- mounts.append(
75
- f"type=secret,id=uv_index_{name}_password,env={cred['password_env']}"
76
- )
77
- return mounts
78
+ return [
79
+ f"type=secret,id={secret_id},env={environment_name}"
80
+ for secret_id, environment_name in _iter_uv_index_secrets(credentials)
81
+ ]
78
82
 
79
83
 
80
84
  def uv_index_secret_build_args(credentials: list[dict[str, str | None]]) -> list[str]:
81
- """Return ``docker build --secret`` arg pairs for UV index credentials.
85
+ """Return `docker build --secret` arg pairs for UV index credentials.
82
86
 
83
87
  Args:
84
88
  credentials: Output of `collect_uv_index_credentials`.
85
89
 
86
90
  Returns:
87
- A flat list of ``--secret`` flag-value pairs for the docker build command.
91
+ A flat list of `--secret` flag-value pairs for the docker build command.
88
92
  """
89
- args = []
90
- for cred in credentials:
91
- name = cred["index_name"].lower()
92
- if cred["username_env"]:
93
- args.extend(
94
- ["--secret", f"id=uv_index_{name}_username,env={cred['username_env']}"]
95
- )
96
- if cred["password_env"]:
97
- args.extend(
98
- ["--secret", f"id=uv_index_{name}_password,env={cred['password_env']}"]
99
- )
100
- return args
93
+ return [
94
+ item
95
+ for secret_id, environment_name in _iter_uv_index_secrets(credentials)
96
+ for item in ("--secret", f"id={secret_id},env={environment_name}")
97
+ ]
101
98
 
102
99
 
103
100
  def env_truthy(env_var: str) -> bool:
@@ -330,6 +327,28 @@ def resolve_container_docker_build_args(
330
327
  utils.fatal(f"Invalid CONTAINER_DOCKER_BUILD_ARGS: {error}")
331
328
 
332
329
 
330
+ def _resolve_optional_file_path(
331
+ cli_path: str | None,
332
+ environment_path: str | None,
333
+ description: str,
334
+ ) -> Path | None:
335
+ resolved_path = next(
336
+ (
337
+ Path(value.strip())
338
+ for value in (cli_path, environment_path)
339
+ if value and value.strip()
340
+ ),
341
+ None,
342
+ )
343
+ if resolved_path is None:
344
+ return None
345
+ if not resolved_path.exists():
346
+ utils.fatal(f"{description} not found: {resolved_path}")
347
+ if not resolved_path.is_file():
348
+ utils.fatal(f"{description} is not a file: {resolved_path}")
349
+ return resolved_path
350
+
351
+
333
352
  def resolve_container_dockerfile_hook_path(
334
353
  cli_hook_path: str | None, env_hook_path: str | None
335
354
  ) -> Path | None:
@@ -342,17 +361,13 @@ def resolve_container_dockerfile_hook_path(
342
361
  Returns:
343
362
  The validated hook path, or `None` when unset.
344
363
  """
345
- raw_value = (
346
- cli_hook_path if cli_hook_path and cli_hook_path.strip() else env_hook_path
364
+ hook_path = _resolve_optional_file_path(
365
+ cli_hook_path,
366
+ env_hook_path,
367
+ "Dockerfile hook script",
347
368
  )
348
- if raw_value is None or not raw_value.strip():
369
+ if hook_path is None:
349
370
  return None
350
-
351
- hook_path = Path(raw_value.strip())
352
- if not hook_path.exists():
353
- utils.fatal(f"Dockerfile hook script not found: {hook_path}")
354
- if not hook_path.is_file():
355
- utils.fatal(f"Dockerfile hook path is not a file: {hook_path}")
356
371
  if not os.access(hook_path, os.X_OK):
357
372
  utils.fatal(f"Dockerfile hook script must be executable: {hook_path}")
358
373
 
@@ -594,6 +609,28 @@ def get_container_deps_move_script() -> str | None:
594
609
  return inline_script if inline_script else None
595
610
 
596
611
 
612
+ def resolve_container_dockerfile_path(
613
+ dockerfile_path: str | None,
614
+ environment_dockerfile_path: str | None,
615
+ ) -> Path | None:
616
+ """Resolve and validate a project-owned application Dockerfile.
617
+
618
+ Args:
619
+ dockerfile_path: Dockerfile path supplied directly to a task.
620
+ environment_dockerfile_path: Dockerfile path supplied through the
621
+ environment.
622
+
623
+ Returns:
624
+ The configured Dockerfile path, or `None` when the bundled template
625
+ should be used.
626
+ """
627
+ return _resolve_optional_file_path(
628
+ dockerfile_path,
629
+ environment_dockerfile_path,
630
+ "Container Dockerfile",
631
+ )
632
+
633
+
597
634
  def get_prune_keep() -> int:
598
635
  """Return the integer value of CONTAINER_PRUNE_KEEP.
599
636
 
@@ -70,6 +70,28 @@ class _ContainerBuild(NamedTuple):
70
70
  commit_tag: str
71
71
 
72
72
 
73
+ def _prune_container_images(version_tag: str, commit_tag: str) -> None:
74
+ from .docker import prune_images_keep
75
+ from .env import get_prune_keep
76
+ from .utils import get_full_image_name, get_package_name
77
+
78
+ keep = get_prune_keep()
79
+ if keep < 0:
80
+ return
81
+
82
+ LOGGER.debug(
83
+ "Pruning old images; keeping %d and protecting tags: %s",
84
+ keep,
85
+ [version_tag, commit_tag],
86
+ )
87
+ prune_images_keep(
88
+ get_full_image_name(),
89
+ get_package_name(),
90
+ keep,
91
+ protect_tags=[version_tag, commit_tag],
92
+ )
93
+
94
+
73
95
  def _make_var_positional_task_args_optional(
74
96
  func: callable, resolved_task_name: str
75
97
  ) -> None:
@@ -367,11 +389,19 @@ def clean(dist_only: bool = False) -> None:
367
389
  return
368
390
  else:
369
391
  to_clean = [
370
- *[Path(p) for p in [".pytest_cache", "dist", ".mypy_cache"]],
392
+ *[
393
+ Path(p)
394
+ for p in [
395
+ ".pytest_cache",
396
+ "dist",
397
+ ".mypy_cache",
398
+ ".coverage",
399
+ "coverage.xml",
400
+ ".ruff_cache",
401
+ ]
402
+ ],
371
403
  *Path(".").rglob("__pycache__"),
372
404
  *Path(".").rglob("*.pyc"),
373
- Path(".coverage"),
374
- Path("coverage.xml"),
375
405
  ]
376
406
 
377
407
  for item in to_clean:
@@ -406,6 +436,58 @@ def lint_all() -> None:
406
436
  run_command(_get_ruff_command("format", "--check", Path(".")))
407
437
 
408
438
 
439
+ @tasks.script(task_name="docs-build", tags=["docs"])
440
+ def docs_build(config_file: str | None = None, clean: bool = False) -> None:
441
+ """Build the Zensical documentation site in strict mode.
442
+
443
+ Args:
444
+ config_file: Optional path to a Zensical configuration file.
445
+ clean: Remove cached files before building the site.
446
+ """
447
+ from .utils import require_package, run_command
448
+
449
+ require_package("zensical")
450
+ run_command(
451
+ [
452
+ "zensical",
453
+ "build",
454
+ "--strict",
455
+ "--clean" if clean else None,
456
+ "--config-file" if config_file else None,
457
+ config_file,
458
+ ]
459
+ )
460
+
461
+
462
+ @tasks.script(task_name="docs-serve", tags=["docs"])
463
+ def docs_serve(
464
+ config_file: str | None = None,
465
+ dev_addr: str | None = None,
466
+ open_browser: bool = False,
467
+ ) -> None:
468
+ """Serve the Zensical documentation site for local preview.
469
+
470
+ Args:
471
+ config_file: Optional path to a Zensical configuration file.
472
+ dev_addr: Optional development server address in `IP:PORT` form.
473
+ open_browser: Open the preview in the default browser.
474
+ """
475
+ from .utils import require_package, run_command
476
+
477
+ require_package("zensical")
478
+ run_command(
479
+ [
480
+ "zensical",
481
+ "serve",
482
+ "--config-file" if config_file else None,
483
+ config_file,
484
+ "--dev-addr" if dev_addr else None,
485
+ dev_addr,
486
+ "--open" if open_browser else None,
487
+ ]
488
+ )
489
+
490
+
409
491
  @tasks.script(tags=["containers", "build"])
410
492
  def build_image(
411
493
  *docker_build_args: str,
@@ -413,11 +495,12 @@ def build_image(
413
495
  no_cache: bool = False,
414
496
  plain: bool = False,
415
497
  single_arch: bool = False,
498
+ dockerfile_path: str | None = None,
416
499
  dockerfile_hook_path: str | None = None,
417
500
  container_env: list[str] | None = None,
418
501
  container_envfile: list[str] | None = None,
419
502
  ) -> _ContainerBuild:
420
- """Build the container image for this project using the Dockerfile template.
503
+ """Build the container image using a project-owned or bundled Dockerfile.
421
504
 
422
505
  Args:
423
506
  *docker_build_args: Additional arguments passed directly to `docker build`.
@@ -427,8 +510,10 @@ def build_image(
427
510
  no_cache: Do not use cache when building the image.
428
511
  plain: Do not pretty-print output.
429
512
  single_arch: Build images for a single architecture.
513
+ dockerfile_path: Optional project-owned Dockerfile. Overrides
514
+ CONTAINER_DOCKERFILE_PATH if provided.
430
515
  dockerfile_hook_path: Optional executable script path that can mutate
431
- the generated Dockerfile before build. Overrides
516
+ the selected Dockerfile before build. Overrides
432
517
  CONTAINER_DOCKERFILE_HOOK_PATH if provided.
433
518
  container_env: Builder and runtime environment declarations as repeated
434
519
  KEY=VALUE values.
@@ -442,14 +527,10 @@ def build_image(
442
527
  The generated Dockerfile text and image tags.
443
528
  """
444
529
  from .docker import build_image as _build_image
445
- from .docker import (
446
- prune_images_keep,
447
- )
448
530
  from .env import (
449
531
  collect_uv_index_credentials,
450
532
  get_cache_id_suffix,
451
533
  get_container_deps_move_script,
452
- get_prune_keep,
453
534
  get_python_variant,
454
535
  inject_auto_build_args_from_env,
455
536
  load_container_env_tokens,
@@ -459,6 +540,7 @@ def build_image(
459
540
  render_container_deps_move_script,
460
541
  resolve_container_docker_build_args,
461
542
  resolve_container_dockerfile_hook_path,
543
+ resolve_container_dockerfile_path,
462
544
  resolve_extension_content,
463
545
  uv_index_secret_build_args,
464
546
  uv_index_secret_mounts,
@@ -469,14 +551,17 @@ def build_image(
469
551
  )
470
552
  from .utils import (
471
553
  fatal,
472
- get_full_image_name,
473
- get_package_name,
474
554
  load_data_file,
475
555
  render_template_text,
476
556
  )
477
557
 
558
+ resolved_dockerfile_path = resolve_container_dockerfile_path(
559
+ dockerfile_path,
560
+ os.getenv("CONTAINER_DOCKERFILE_PATH"),
561
+ )
562
+
478
563
  # Determine extensions up-front so we can log a single, accurate message
479
- extensions = parse_container_extensions()
564
+ extensions = [] if resolved_dockerfile_path else parse_container_extensions()
480
565
  extension_ids = [desc.get("id") for desc in extensions if desc.get("id")]
481
566
  if extension_ids:
482
567
  LOGGER.info("Building image (with extensions: %s)", ", ".join(extension_ids))
@@ -504,6 +589,29 @@ def build_image(
504
589
  dockerfile_hook_path,
505
590
  os.getenv("CONTAINER_DOCKERFILE_HOOK_PATH"),
506
591
  )
592
+ top_level_build_args = inject_auto_build_args_from_env(
593
+ {"PYTHON_VARIANT": get_python_variant()}
594
+ )
595
+
596
+ if resolved_dockerfile_path:
597
+ version_tag, commit_tag = _build_image(
598
+ dockerfile_path=resolved_dockerfile_path,
599
+ context_path=Path("."),
600
+ debug=debug,
601
+ no_cache=no_cache,
602
+ plain=plain,
603
+ single_arch=single_arch,
604
+ omit_target=not debug,
605
+ extra_build_args=top_level_build_args or None,
606
+ docker_build_args=resolved_docker_build_args,
607
+ dockerfile_hook_path=resolved_dockerfile_hook_path,
608
+ )
609
+ _prune_container_images(version_tag, commit_tag)
610
+ return _ContainerBuild(
611
+ dockerfile_text=resolved_dockerfile_path.read_text(encoding="utf-8"),
612
+ version_tag=version_tag,
613
+ commit_tag=commit_tag,
614
+ )
507
615
 
508
616
  has_debug_deps = has_debug_dependency_group()
509
617
  if debug and not has_debug_deps:
@@ -527,9 +635,6 @@ def build_image(
527
635
  "Injecting builder and runtime env vars: %s",
528
636
  ", ".join(container_env_vars),
529
637
  )
530
- top_level_build_args = inject_auto_build_args_from_env(
531
- {"PYTHON_VARIANT": get_python_variant()}
532
- )
533
638
  external_deps_image = _get_container_build_setting("CONTAINER_DEPS_IMAGE")
534
639
  if external_deps_image:
535
640
  top_level_build_args["CONTAINER_DEPS_IMAGE"] = external_deps_image
@@ -639,16 +744,7 @@ def build_image(
639
744
  dockerfile_hook_path=resolved_dockerfile_hook_path,
640
745
  )
641
746
 
642
- keep = get_prune_keep()
643
- if keep >= 0:
644
- # Protect the tags created by this build
645
- protect = [t for t in (version_tag, commit_tag) if t is not None]
646
- LOGGER.debug(
647
- "Pruning old images; keeping %d and protecting tags: %s", keep, protect
648
- )
649
- prune_images_keep(
650
- get_full_image_name(), get_package_name(), keep, protect_tags=protect
651
- )
747
+ _prune_container_images(version_tag, commit_tag)
652
748
 
653
749
  return _ContainerBuild(
654
750
  dockerfile_text=dockerfile_text,
@@ -1268,6 +1364,7 @@ def _run_release_flow(
1268
1364
  no_cache: bool = False,
1269
1365
  plain: bool = False,
1270
1366
  single_arch: bool = False,
1367
+ dockerfile_path: str | None = None,
1271
1368
  container_env: list[str] | None = None,
1272
1369
  container_envfile: list[str] | None = None,
1273
1370
  assets: list[str] | None = None,
@@ -1374,6 +1471,7 @@ def _run_release_flow(
1374
1471
  no_cache=no_cache,
1375
1472
  plain=plain,
1376
1473
  single_arch=single_arch,
1474
+ dockerfile_path=dockerfile_path,
1377
1475
  container_env=container_env,
1378
1476
  container_envfile=container_envfile,
1379
1477
  )
@@ -1392,6 +1490,7 @@ def release(
1392
1490
  no_cache: bool = False,
1393
1491
  plain: bool = False,
1394
1492
  single_arch: bool = False,
1493
+ dockerfile_path: str | None = None,
1395
1494
  container_env: list[str] | None = None,
1396
1495
  container_envfile: list[str] | None = None,
1397
1496
  assets: list[str] | None = None,
@@ -1410,6 +1509,8 @@ def release(
1410
1509
  no_cache: Do not use cache when building container images.
1411
1510
  plain: Do not pretty-print container build output.
1412
1511
  single_arch: Build container image for a single architecture.
1512
+ dockerfile_path: Optional project-owned Dockerfile used for the image
1513
+ build. Overrides CONTAINER_DOCKERFILE_PATH if provided.
1413
1514
  container_env: Inline container environment variables as repeated
1414
1515
  KEY=VALUE values.
1415
1516
  container_envfile: Repeated list of container environment files.
@@ -1428,6 +1529,7 @@ def release(
1428
1529
  no_cache=no_cache,
1429
1530
  plain=plain,
1430
1531
  single_arch=single_arch,
1532
+ dockerfile_path=dockerfile_path,
1431
1533
  container_env=container_env,
1432
1534
  container_envfile=container_envfile,
1433
1535
  assets=assets,
@@ -1480,6 +1582,7 @@ def build(
1480
1582
  no_cache: bool = False,
1481
1583
  plain: bool = False,
1482
1584
  single_arch: bool = False,
1585
+ dockerfile_path: str | None = None,
1483
1586
  container_env: list[str] | None = None,
1484
1587
  container_envfile: list[str] | None = None,
1485
1588
  ) -> None:
@@ -1492,6 +1595,8 @@ def build(
1492
1595
  no_cache: Pass `--no-cache` to the Docker build command.
1493
1596
  plain: Pass `--progress plain` to the Docker build command.
1494
1597
  single_arch: Build the container for the current host architecture only.
1598
+ dockerfile_path: Optional project-owned Dockerfile used for the image
1599
+ build. Overrides CONTAINER_DOCKERFILE_PATH if provided.
1495
1600
  container_env: Inline container environment variables as repeated
1496
1601
  `KEY=VALUE` values.
1497
1602
  container_envfile: Repeated list of container environment files.
@@ -1506,6 +1611,7 @@ def build(
1506
1611
  no_cache=no_cache,
1507
1612
  plain=plain,
1508
1613
  single_arch=single_arch,
1614
+ dockerfile_path=dockerfile_path,
1509
1615
  container_env=container_env,
1510
1616
  container_envfile=container_envfile,
1511
1617
  )
@@ -1520,6 +1626,7 @@ def build_with_containers(
1520
1626
  no_cache: bool = False,
1521
1627
  plain: bool = False,
1522
1628
  single_arch: bool = False,
1629
+ dockerfile_path: str | None = None,
1523
1630
  container_env: list[str] | None = None,
1524
1631
  container_envfile: list[str] | None = None,
1525
1632
  ) -> None:
@@ -1530,6 +1637,8 @@ def build_with_containers(
1530
1637
  no_cache: Do not use cache when building the image.
1531
1638
  plain: Do not pretty-print output.
1532
1639
  single_arch: Build images for a single architecture.
1640
+ dockerfile_path: Optional project-owned Dockerfile used for the image
1641
+ build. Overrides CONTAINER_DOCKERFILE_PATH if provided.
1533
1642
  container_env: Inline container environment variables as repeated KEY=VALUE
1534
1643
  values.
1535
1644
  container_envfile: Repeated list of container environment files.
@@ -1541,6 +1650,7 @@ def build_with_containers(
1541
1650
  no_cache=no_cache,
1542
1651
  plain=plain,
1543
1652
  single_arch=single_arch,
1653
+ dockerfile_path=dockerfile_path,
1544
1654
  container_env=container_env,
1545
1655
  container_envfile=container_envfile,
1546
1656
  )
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.5
2
+ Name: common-python-tasks
3
+ Version: 0.11.1
4
+ Summary: Opinionated Poe the Poet tasks for Python package development.
5
+ Project-URL: Homepage, http://github.com/ci-sourcerer/common-python-tasks
6
+ Project-URL: Source, http://github.com/ci-sourcerer/common-python-tasks.git
7
+ Project-URL: Issues, http://github.com/ci-sourcerer/common-python-tasks/issues
8
+ Author-email: Joseph Asbury <ci_sourcerer@yahoo.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Build Tools
19
+ Requires-Python: <4.0,>=3.11
20
+ Requires-Dist: docker<8.0.0,>=7.1.0
21
+ Requires-Dist: dunamai<2.0.0,>=1.25.0
22
+ Requires-Dist: git-cliff<3.0.0,>=2.12.0
23
+ Requires-Dist: jinja2<4.0.0,>=3.1.6
24
+ Requires-Dist: packaging<27.0,>=26.2
25
+ Requires-Dist: poethepoet-tasks<0.4.0,>=0.3.0
26
+ Requires-Dist: pytest-cov<8.0.0,>=7.0.0
27
+ Requires-Dist: pytest<10.0.0,>=9.0.3
28
+ Requires-Dist: requests<3.0.0,>=2.34.2
29
+ Requires-Dist: ruff<0.17.0,>=0.16.1
30
+ Requires-Dist: tomlkit<0.14.0,>=0.13.3
31
+ Requires-Dist: zensical<0.1.0,>=0.0.59
32
+ Description-Content-Type: text/markdown
33
+
34
+ # Common Python Tasks
35
+
36
+ `common-python-tasks` provides reusable, opinionated [Poe the Poet](https://poethepoet.natn.io/guides/packaged_tasks.html) tasks for Python development, packaging, releases, containers, and documentation.
37
+
38
+ Read the [complete documentation](https://ci-sourcerer.github.io/common-python-tasks/) for the task reference, configuration options, and deployment guidance.
39
+
40
+ ## Quick start
41
+
42
+ Add the package as a development dependency.
43
+
44
+ ```shell
45
+ uv add --dev common-python-tasks==0.11.1
46
+ ```
47
+
48
+ Expose the default task set in `pyproject.toml`.
49
+
50
+ ```toml
51
+ [tool.poe]
52
+ include_script = "common_python_tasks:tasks()"
53
+ ```
54
+
55
+ Run the everyday development tasks.
56
+
57
+ ```shell
58
+ poe format
59
+ poe lint
60
+ poe test
61
+ ```
62
+
63
+ ## Optional workflows
64
+
65
+ Select additional task groups with `include_tags`.
66
+
67
+ ```toml
68
+ [tool.poe]
69
+ include_script = "common_python_tasks:tasks(include_tags=['common', 'docs', 'containers'])"
70
+ ```
71
+
72
+ | Tag | Purpose |
73
+ | - | - |
74
+ | `common` | Formatting, linting, testing, packaging, and releases |
75
+ | `docs` | Build and serve Zensical documentation |
76
+ | `containers` | Build, run, inspect, and publish container images |
77
+ | `fastapi` | Run the FastAPI and PostgreSQL development stack |
78
+
79
+ Documentation projects can build locally or start a development server.
80
+
81
+ ```shell
82
+ poe docs-build
83
+ poe docs-serve
84
+ ```
85
+
86
+ The [documentation workflow guide](https://ci-sourcerer.github.io/common-python-tasks/documentation-workflows/) covers GitHub Actions artifacts, optional Cloudflare Pages previews, and GitHub Pages deployment.
87
+
88
+ ## Learn more
89
+
90
+ - [Getting started](https://ci-sourcerer.github.io/common-python-tasks/getting-started/)
91
+ - [Task reference](https://ci-sourcerer.github.io/common-python-tasks/tasks/)
92
+ - [Configuration reference](https://ci-sourcerer.github.io/common-python-tasks/configuration/)
93
+
94
+ The project is in alpha, so minor releases may contain breaking changes. It is distributed under the [MIT license](LICENSE).
@@ -2,11 +2,11 @@ common_python_tasks/__init__.py,sha256=tqRLgOwmMQHDb1A84XbML9sWfzMymkSPhJjoF6aCH
2
2
  common_python_tasks/__main__.py,sha256=GpgSd_toMbEaWA_cZvgsZE7jo4XpzuI-awBVm2uhVyw,3512
3
3
  common_python_tasks/docker.py,sha256=_YW-hyHWlUK-kMCvI6QTgxSfmXQQKhYLOKRBPfUmtHA,24176
4
4
  common_python_tasks/docker_compose.py,sha256=NGgMei3oCYS0RbNaQXE-2veXFylO3XtZ7lU-PTtocik,21338
5
- common_python_tasks/env.py,sha256=1YLSxsUP9lPn51_zraM3nAiN1J7aKdUHzjzDzxXLo34,19696
5
+ common_python_tasks/env.py,sha256=KxXyvQcSe_zMy4drTWPX3-R_q7u4jlwVNMDkRaTta-M,20666
6
6
  common_python_tasks/git.py,sha256=0X472w_vHdtbrHHxCmJ85NoQjjogEEDs_0SRXfxbWi0,11719
7
7
  common_python_tasks/github.py,sha256=_LVanl-z4PsdLM94L8y7tkX-AZO2n3Qy39FRY7reDhA,21589
8
8
  common_python_tasks/project.py,sha256=PQ_GQicTrSQgVDB9cJ9E6pU0UhLPom2xou1sGFscr3E,17377
9
- common_python_tasks/tasks.py,sha256=h-bPYwTZ3uct4dR8a-NUYKHhSB4EZ-J9iJyhaB3yJc4,60135
9
+ common_python_tasks/tasks.py,sha256=U6CGlLVW_E-9Hofja21bUwiiISU05GdJbTTzI7Vwv2I,63796
10
10
  common_python_tasks/utils.py,sha256=hR7w0Cb7uQ7ILyIegTQ4znWHVoWANlCgxrOnO-Rh5oE,20798
11
11
  common_python_tasks/data/dockerfile_extensions/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  common_python_tasks/data/fastapi/alembic.ini.j2,sha256=dOiAmZngzX6eyVXeM6BhzgQmes2zwUEdKwOgm8mxjMs,985
@@ -21,7 +21,7 @@ common_python_tasks/data/generic/Dockerfile.j2,sha256=hgLrIRU5xvS2xqw9Xm4kfkcBGH
21
21
  common_python_tasks/data/generic/cliff.toml,sha256=po1MnEGEEgrpyBz3XLWr0h7emXb6ASX63lrU2r6o240,77
22
22
  common_python_tasks/data/generic/pytest.ini,sha256=RHak5QrLzKHvUBjFmaaiOYFrd_8YBRQubXEOT0NJfco,134
23
23
  common_python_tasks/data/generic/ruff.toml,sha256=njvd-5inSGmgY865SWCT4SR9IrKlzS7JQFZmAG2UT_E,53
24
- common_python_tasks-0.10.3.dist-info/METADATA,sha256=p9oMXue96WswNU4KsUoG8yBP8GZBsyQqqlFfpkMojHk,22464
25
- common_python_tasks-0.10.3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
26
- common_python_tasks-0.10.3.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
27
- common_python_tasks-0.10.3.dist-info/RECORD,,
24
+ common_python_tasks-0.11.1.dist-info/METADATA,sha256=v1Gf_iMcyv-ZWpREpc9ji35TtQ9T8oyEmdKPga5yyak,3307
25
+ common_python_tasks-0.11.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
26
+ common_python_tasks-0.11.1.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
27
+ common_python_tasks-0.11.1.dist-info/RECORD,,
@@ -1,442 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: common-python-tasks
3
- Version: 0.10.3
4
- Summary: Opinionated Poe the Poet tasks for Python package development.
5
- Project-URL: Homepage, http://github.com/ci-sourcerer/common-python-tasks
6
- Project-URL: Source, http://github.com/ci-sourcerer/common-python-tasks.git
7
- Project-URL: Issues, http://github.com/ci-sourcerer/common-python-tasks/issues
8
- Author-email: Joseph Asbury <ci_sourcerer@yahoo.com>
9
- License-Expression: MIT
10
- License-File: LICENSE
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
17
- Classifier: Programming Language :: Python :: 3.14
18
- Classifier: Topic :: Software Development :: Build Tools
19
- Requires-Python: <4.0,>=3.11
20
- Requires-Dist: docker<8.0.0,>=7.1.0
21
- Requires-Dist: dunamai<2.0.0,>=1.25.0
22
- Requires-Dist: git-cliff<3.0.0,>=2.12.0
23
- Requires-Dist: jinja2<4.0.0,>=3.1.6
24
- Requires-Dist: packaging<27.0,>=26.2
25
- Requires-Dist: poethepoet-tasks<0.4.0,>=0.3.0
26
- Requires-Dist: pytest-cov<8.0.0,>=7.0.0
27
- Requires-Dist: pytest<10.0.0,>=9.0.3
28
- Requires-Dist: requests<3.0.0,>=2.34.2
29
- Requires-Dist: ruff<0.17.0,>=0.16.1
30
- Requires-Dist: tomlkit<0.14.0,>=0.13.3
31
- Description-Content-Type: text/markdown
32
-
33
- # Common Python Tasks
34
-
35
- `common-python-tasks` provides reusable, opinionated [Poe the Poet](https://poethepoet.natn.io/guides/packaged_tasks.html) tasks for common Python development workflows.
36
-
37
- It supplies sensible defaults for formatting, linting, testing, packaging, releases, and container workflows while allowing projects to override configuration when needed.
38
-
39
- ## Quick start
40
-
41
- ### Manual setup
42
-
43
- Add `common-python-tasks` as a development dependency from your project root.
44
-
45
- ```shell
46
- uv add --dev common-python-tasks==0.10.3
47
- ```
48
-
49
- Configure Poe the Poet to expose the default `common` task set.
50
-
51
- ```toml
52
- [tool.poe]
53
- include_script = "common_python_tasks:tasks()"
54
- ```
55
-
56
- Run the common development tasks.
57
-
58
- ```shell
59
- poe format
60
- poe lint
61
- poe test
62
- ```
63
-
64
- ### Automated setup
65
-
66
- The helper script performs the same development-dependency installation and Poe configuration. Download the script for the exact release you want, review it, then run it from your project root.
67
-
68
- ```shell
69
- curl --fail --silent --show-error --location \
70
- --output /tmp/add_common_python_tasks.py \
71
- https://raw.githubusercontent.com/ci-sourcerer/common-python-tasks/v0.10.3/scripts/add_common_python_tasks.py
72
- ```
73
-
74
- Review the downloaded script before executing it.
75
-
76
- ```shell
77
- less /tmp/add_common_python_tasks.py
78
- ```
79
-
80
- Run the reviewed script with the same pinned package version.
81
-
82
- ```shell
83
- COMMON_PYTHON_TASKS_VERSION=0.10.3 python3 /tmp/add_common_python_tasks.py
84
- ```
85
-
86
- To install another release, replace both occurrences of `0.10.3` with that release's version.
87
-
88
- ## Available tasks
89
-
90
- The generated tables below list public tasks only. Tags identify which tasks are selected by `include_tags` and `exclude_tags`.
91
-
92
- <!-- tasks-table -->
93
-
94
- ### Daily development
95
-
96
- | Task | Description | Tags |
97
- | --- | --- | --- |
98
- | `test` | Run the test suite with coverage (if pytest-cov is installed). | common, test |
99
- | `clean` | Clean up temporary files and directories. | clean, common |
100
- | `format` | Fix import issues and format Python code with Ruff. | common, format |
101
- | `lint` | Check Python lint and formatting with Ruff. | common, lint |
102
-
103
- ### Packaging and releases
104
-
105
- | Task | Description | Tags |
106
- | --- | --- | --- |
107
- | `publish-package` | Publish the package to the PyPI server. | common, packaging |
108
- | `publish-github-release` | Publish or update a GitHub Release for the current repository. | common, packaging, release |
109
- | `update-dependencies` | Update project dependencies with uv. | common, packaging |
110
- | `build-package` | Build the package (wheel and sdist). | build, common, packaging |
111
- | `bump-version` | Bump the project version. | common, packaging |
112
- | `changelog` | Print the changelog for the current version based on git history and git-cliff. | common, packaging, release |
113
- | `release` | Run a full release flow for package and containers. | common, containers, packaging, release |
114
-
115
- ### Container images
116
-
117
- | Task | Description | Tags |
118
- | --- | --- | --- |
119
- | `build-image` | Build the container image for this project using the Dockerfile template. | build, containers |
120
- | `build-deps-image` | Build only the container dependency collector image for this project. | build, containers |
121
- | `run-container` | Run the Docker image as a container for this project. By default, this will run the most-recently-built tag for the project's image. | containers |
122
- | `push-image` | Push the Docker image for this project to the container registry. | containers, packaging, release |
123
- | `build` | Build the project and its containers. | common, containers, packaging |
124
- | `container-shell` | Run the debug image with an interactive shell. | containers, debug |
125
-
126
- ### Development stacks
127
-
128
- | Task | Description | Tags |
129
- | --- | --- | --- |
130
- | `stack-up` | Bring up the development stack for the application. | containers, fastapi, web |
131
- | `stack-down` | Bring down the development stack for the application. | containers, fastapi, web |
132
- | `reset-db` | Reset the database by deleting the database volume. | containers, database, fastapi, web |
133
- | `run-db-migrations` | Run database migrations. | containers, database, fastapi, web |
134
- | `db-shell` | Open a psql shell to the database container. | containers, database, fastapi, web |
135
-
136
- <!-- end-tasks-table -->
137
-
138
- ## Configuration and requirements
139
-
140
- ### Requirements
141
-
142
- Every project needs a `pyproject.toml` file at its root and Poe the Poet available in its development environment.
143
-
144
- Tasks that install, build, publish, or update dependencies require uv. Package and release tasks need a resolvable project version from `project.version` or Git tags. Dynamic versioning is supported but is not a baseline requirement.
145
-
146
- ### Task selection
147
-
148
- Calling `tasks()` without arguments exposes the default `common` task set.
149
-
150
- ```toml
151
- [tool.poe]
152
- include_script = "common_python_tasks:tasks()"
153
- ```
154
-
155
- Select optional task groups with tags when your project needs them.
156
-
157
- ```toml
158
- [tool.poe]
159
- include_script = "common_python_tasks:tasks(include_tags=['common', 'containers'])"
160
- ```
161
-
162
- The `containers` tag also includes container-based development-stack tasks. Use the tags in the task tables to tailor a smaller task set.
163
-
164
- ### Package management
165
-
166
- Packaging-related tasks, including `build-package`, `publish-package`, dependency updates, release version resolution, and container build metadata, use uv exclusively. The `uv` executable must be available on `PATH`.
167
-
168
- ### Publish target selection
169
-
170
- The `publish-package` task resolves publish targets with this precedence.
171
-
172
- 1. Explicit task arguments (`repository` or `repository_url`)
173
- 2. Environment-variable fallback
174
- 3. uv project configuration fallback from `[[tool.uv.index]]`
175
- 4. The `uv publish` default
176
-
177
- uv prefers named publish indexes.
178
-
179
- - `COMMON_PYTHON_TASKS_PUBLISH_REPOSITORY`
180
- - `UV_PUBLISH_INDEX`
181
- - `COMMON_PYTHON_TASKS_PUBLISH_URL`
182
- - `UV_PUBLISH_URL`
183
-
184
- When using uv project configuration fallback, `[[tool.uv.index]]` must include both `name` and `publish-url` for publish selection. If multiple publishable indexes exist, set exactly one `default = true` or pass an explicit repository.
185
-
186
- ### Configuration precedence
187
-
188
- For pytest and coverage, configuration resolves in the following order.
189
-
190
- 1. Matching `pyproject.toml` sections such as `[tool.pytest.ini_options]` and `[tool.coverage]`
191
- 2. Environment variables that specify a configuration path
192
- 3. Local configuration files in the project root
193
- 4. Bundled defaults from [`src/common_python_tasks/data`](src/common_python_tasks/data)
194
-
195
- Ruff uses `RUFF_CONFIG` when set. Otherwise it discovers `.ruff.toml`, `ruff.toml`, or a `[tool.ruff]` section in `pyproject.toml`. When no project configuration is available, the tasks use the bundled Ruff defaults. Ruff configuration follows Ruff's native file precedence, so `.ruff.toml` takes precedence over `ruff.toml`, which takes precedence over `pyproject.toml` in the same directory.
196
-
197
- The `format` task first applies safe fixes for unused imports and import sorting, then runs the Ruff formatter. The `lint` task checks both Ruff lint rules and formatting without changing files.
198
-
199
- ### Configuration examples
200
-
201
- After installing the package, a minimal project configuration looks like this.
202
-
203
- ```toml
204
- [project]
205
- name = "simple-cli-tool"
206
- version = "0.10.3"
207
-
208
- [tool.poe]
209
- include_script = "common_python_tasks:tasks()"
210
- ```
211
-
212
- A container-based project can select the container task group and set its image details.
213
-
214
- ```toml
215
- [tool.poe]
216
- include_script = "common_python_tasks:tasks(include_tags=['common', 'containers'])"
217
-
218
- [tool.poe.env]
219
- CONTAINER_REGISTRY_USERNAME = "myusername"
220
- PACKAGE_NAME = "containerized-app"
221
- ```
222
-
223
- The `test` task automatically uses pytest configuration in `pyproject.toml`.
224
-
225
- ```toml
226
- [tool.pytest.ini_options]
227
- testpaths = ["tests", "integration"]
228
- addopts = "-ra"
229
- ```
230
-
231
- ### Environment variables
232
-
233
- #### Task configuration files
234
-
235
- - `PYTEST_CONFIG`: Path to the pytest configuration file
236
- - `COVERAGE_RCFILE`: Path to the coverage configuration file
237
- - `RUFF_CONFIG`: Path to a Ruff TOML configuration file
238
-
239
- #### Project and package settings
240
-
241
- - `PACKAGE_NAME`: Overrides the package name inferred from `pyproject.toml`
242
- - `COMMON_PYTHON_TASKS_PUBLISH_REPOSITORY`: Preferred publish repository/index name for `publish-package`
243
- - `COMMON_PYTHON_TASKS_PUBLISH_URL`: Preferred uv publish upload URL for `publish-package` when no repository/index is selected
244
- - `UV_PUBLISH_INDEX`: uv publish-index fallback for `publish-package`
245
- - `UV_PUBLISH_URL`: uv publish upload-url fallback for `publish-package`
246
-
247
- #### Container image settings
248
-
249
- - `CONTAINER_REGISTRY_USERNAME`: Container-registry username for image tagging; the default is the current local user
250
- - `CONTAINER_REGISTRY_URL`: Registry URL with a default of `docker.io/{username}`
251
- - `CONTAINER_PYTHON_VARIANT`: Python base-image variant such as `slim`, `alpine`, etc. See <https://hub.docker.com/_/python> for available options. Defaults to `slim`; set to empty string for no variant (e.g., `FROM python:3.11`). The value is passed as the `PYTHON_VARIANT` Docker build argument and recorded in the `org.opencontainers.image.python.variant` image label.
252
- - `CONTAINER_DOCKER_BUILD_ARGS`: Additional arguments passed directly to `docker build`, parsed using shell quoting rules. Free arguments provided to the task after `--` take precedence.
253
- - `CONTAINER_DOCKERFILE_HOOK_PATH`: Optional host path to an executable hook script that receives the generated Dockerfile path and can modify the file before `docker build` runs.
254
- - `CONTAINER_APT_PACKAGES`: Space-delimited system packages installed in the generated image
255
- - `CONTAINER_CUSTOM_ENTRYPOINT`: Custom container entrypoint script. The value must match a key in `[project].scripts`.
256
- - `CONTAINER_DEPS_IMAGE`: Existing dependency image used when neither `CONTAINER_DEPS_CONTENT` nor `CONTAINER_DEPS_FILE` is configured
257
- - `CONTAINER_EXTENSION_FILES`: Colon-delimited local extension Dockerfile paths. Escape literal colons as `\:` or quote the complete path.
258
- - `CONTAINER_EXTENSIONS`: Colon-delimited extension-bundle names or parameterized values. Escape literal colons as `\:` or quote the complete value.
259
- - `CONTAINER_ENV`: Colon-delimited `KEY=VALUE` declarations injected into both the builder and runtime stages. These variables are available to package build commands and runtime-stage build commands, and persist in the final and debug images for running containers. The `container_env` and `container_envfile` task arguments use the same behavior. Escape literal colons as `\:` or quote the complete value. Use `run-container --env` or `--envfile` to override values when starting a container.
260
- - `.containerenv`: A project-root file that can supply the same declarations. It is loaded before the `container_envfile` task argument, `CONTAINER_ENV`, and the `container_env` task argument.
261
- - `CONTAINER_PRUNE_KEEP`: Image-pruning policy after builds. `-1` keeps all images, `0` keeps only the latest, and `N` keeps the latest plus `N` prior images.
262
- - `CONTAINER_DEPS_CONTENT`: Inline Dockerfile instructions for a dependency image that installs artifacts into `/tmp/deps`
263
- - `CONTAINER_DEPS_FILE`: One or more dependency-image Dockerfiles. It accepts colon-delimited paths with literal colons escaped as `\:` and is used only when `CONTAINER_DEPS_CONTENT` is unset.
264
- - `CONTAINER_DEPS_MAPPINGS`: Space-delimited `name:/target/path` entries for copying items from `/tmp/deps`. It is used only when no dependency move script is set.
265
- - `CONTAINER_DEPS_MOVE_SCRIPT`: Raw executable script to run after `/tmp/deps` is copied into the image
266
- - `CONTAINER_DEPS_MOVE_SCRIPT_PATH`: Host path to a dependency move script. This takes precedence over `CONTAINER_DEPS_MOVE_SCRIPT`.
267
- - `UV_INDEX_{name}_USERNAME` and `UV_INDEX_{name}_PASSWORD`: Private Python index credentials consumed by uv during the Docker build. When any of these are set, the task automatically passes them as BuildKit secrets (`--secret id=uv_index_{name}_username,env=...`) and renders matching `--mount=type=secret` directives in the builder stage so uv can authenticate without baking credentials into the image. Multiple indices are supported; replace `{name}` with the uppercase index name (hyphens as underscores). These can be set in `tool.poe.env` for CI/CD or in the local environment for development.
268
-
269
- #### Release settings
270
-
271
- - `GITHUB_RELEASE_ASSETS`: Colon-delimited file paths or glob patterns to attach to a GitHub Release. The default is `dist/*`; escape literal colons as `\:` or quote the complete path.
272
- - `SKIP_GITHUB_RELEASE`: Truthy value that skips GitHub Release publication
273
- - `GITHUB_TOKEN` or `GH_TOKEN`: GitHub authentication token for releases and assets
274
- - `GITHUB_REPOSITORY`: Optional repository-slug override for GitHub Release publication
275
- - `GITHUB_API_URL` and `GITHUB_SERVER_URL`: GitHub Enterprise API-host settings
276
- - `GITHUB_RELEASE_TAG`: Optional release tag name
277
- - `GITHUB_RELEASE_NAME`: Optional release title
278
- - `GITHUB_RELEASE_BODY`: Optional release body
279
- - `RELEASE_UPDATE_CHANGELOG`: Truthy value that prepends `git-cliff --unreleased --tag "$RELEASE_TAG"` output to `CHANGELOG.md` before the release tag is created. It is enabled by default.
280
- - `RELEASE_PRE_SCRIPT`: Optional shell command to run before release steps
281
- - `RELEASE_POST_SCRIPT`: Optional shell command to run after release completion
282
- - Release hooks receive `RELEASE_SCRIPT_PHASE`, `RELEASE_TAG`, `RELEASE_VERSION`, `RELEASE_STAGE`, `RELEASE_COMPONENT`, and `RELEASE_DRY_RUN`.
283
-
284
- #### Docker Compose settings
285
-
286
- - `COMPOSE_TYPE`: Application-stack type, such as `fastapi`
287
- - `COMPOSE_ADDONS`: Colon-delimited services to include, such as `db`
288
- - `COMPOSE_FILE`: Override for all compose files with colon-delimited paths
289
- - `COMPOSE_OVERLAY_FILES`: Additional compose files to merge with colon-delimited paths
290
- - `API_PORT`: API server port with a default of `8080`
291
- - `SECRET_KEY`: Application secret key generated automatically when unset
292
- - `ENVIRONMENT`: Environment name with a default of `production`
293
- - `DEBUG_PORT`: Python debugger port in debug mode with a default of `5678`
294
- - `DB_PORT`: Published host port for PostgreSQL, defaulting to `5432`. The API and migrator connect to the database container on port `5432` regardless of this setting.
295
- - `DB_USER`: Database user with a default of the package name
296
- - `DB_BASE`: Database name with a default of the package name
297
- - `DB_PASS`: Database password generated automatically when unset
298
- - `POSTGRES_VERSION`: PostgreSQL version with a default of `17`
299
- - `ADMINER_PORT`: Adminer web UI port with a default of `8081`
300
-
301
- #### Debugging
302
-
303
- - `COMMON_PYTHON_TASKS_LOG_LEVEL`: Set to `DEBUG` to show detailed configuration resolution
304
-
305
- ## Containers and development stacks
306
-
307
- Docker Compose development-stack tasks are available when the `containers` tag is selected. The current stack supports FastAPI applications and an optional PostgreSQL database.
308
-
309
- ### Native Docker build arguments
310
-
311
- Arguments after the task's `--` separator are passed directly to `docker build`. Docker performs option validation, so any supported build option and its value can be used without a package-specific allowlist.
312
-
313
- ```shell
314
- poe build-image --single-arch -- \
315
- --secret id=pip_conf,env=PIP_CONF \
316
- --ssh default \
317
- --add-host example:127.0.0.1
318
- ```
319
-
320
- Set `CONTAINER_DOCKER_BUILD_ARGS` to provide the same arguments through the environment. The value uses shell quoting rules, and task arguments provided after `--` take precedence.
321
-
322
- Settings that affect generated Dockerfile content or dependency-image orchestration can be persisted directly in the project configuration.
323
-
324
- ```toml
325
- [tool.poe.env]
326
- CONTAINER_APT_PACKAGES = "curl jq"
327
- CONTAINER_CUSTOM_ENTRYPOINT = "serve"
328
- CONTAINER_DEPS_IMAGE = "example/dependencies:latest"
329
- ```
330
-
331
- ### Stack configuration
332
-
333
- Set `COMPOSE_TYPE` to select the application stack. `fastapi` is currently supported and includes optional database support and Alembic migrations.
334
-
335
- ```toml
336
- [tool.poe.env]
337
- COMPOSE_TYPE = "fastapi"
338
- ```
339
-
340
- Set `COMPOSE_ADDONS` to select extra services. Addon names are colon-delimited, and `db` is the currently available addon.
341
-
342
- ```toml
343
- [tool.poe.env]
344
- COMPOSE_ADDONS = "db"
345
- ```
346
-
347
- ### Compose-file customization
348
-
349
- The compose setup resolves files in this precedence order.
350
-
351
- 1. **Environment override** uses `COMPOSE_FILE` with colon-delimited paths.
352
- 2. **Automatically loaded files** are based on `COMPOSE_TYPE` and `COMPOSE_ADDONS`.
353
- - `compose-base.yml` provides the core application service.
354
- - `compose-{addon}.yml` adds one file per addon, such as `compose-db.yml`.
355
- - `compose-debug.yml` is used when the `--debug` flag is present.
356
- - `compose-{addon}-debug.yml` provides debug overlays for addons.
357
- 3. **Additional overlays** use `COMPOSE_OVERLAY_FILES` with colon-delimited paths.
358
-
359
- You can provide local compose files or allow the tasks to use bundled templates.
360
-
361
- ### FastAPI stack
362
-
363
- The FastAPI stack uses the standard Dockerfile supplied by this package. Configure its ports, credentials, and database settings through the [Docker Compose settings](#docker-compose-settings) reference.
364
-
365
- ## Troubleshooting
366
-
367
- ### Tasks not showing up with `poe --help`
368
-
369
- Check the `[tool.poe]` configuration in `pyproject.toml` and use `include_script`.
370
-
371
- ```toml
372
- # Correct
373
- [tool.poe]
374
- include_script = "common_python_tasks:tasks(exclude_tags=['internal'])"
375
-
376
- # Incorrect
377
- [tool.poe]
378
- includes = "common_python_tasks:tasks"
379
- ```
380
-
381
- ### Config files not being used
382
-
383
- Review the [configuration precedence](#configuration-precedence) and enable debug logging to see the selected configuration.
384
-
385
- ```shell
386
- COMMON_PYTHON_TASKS_LOG_LEVEL=DEBUG poe test
387
- ```
388
-
389
- ### GitHub Release assets not uploading
390
-
391
- Confirm that `dist/` contains the built wheels and source distributions. You can override the default asset selection with `GITHUB_RELEASE_ASSETS`.
392
-
393
- ```shell
394
- GITHUB_RELEASE_ASSETS="dist/*.whl:dist/*.tar.gz" poe publish-github-release
395
- ```
396
-
397
- ### Container build fails with "unable to find package"
398
-
399
- Check that `pyproject.toml` has a correct package name and the package-discovery settings required by its build backend. For a `src` layout built with Hatch, configure `[tool.hatch.build.targets.wheel] packages = ["src/your_package"]`.
400
-
401
- ### Stack fails to start or services cannot connect
402
-
403
- Check the following conditions.
404
-
405
- - `COMPOSE_TYPE` is set and the required addon is selected.
406
- - Default ports are available, including `8080` for the API, `5432` for PostgreSQL, and `8081` for Adminer.
407
- - The Docker daemon is running, as verified with `docker info`.
408
- - Service logs are available through `docker compose logs` in the project directory.
409
-
410
- ### Database migrations fail
411
-
412
- Verify that the `db` addon is selected, Alembic is configured at the expected location, and its credentials match the generated `.env` values. Use `poe db-shell` to inspect the database manually.
413
-
414
- ### Secrets are not generated
415
-
416
- Ensure the project-root `.env` file is writable and inspect its permissions with `ls -la .env`. You can also generate a value manually with `python -c "import secrets; print(secrets.token_hex(32))"`.
417
-
418
- ## Design choices
419
-
420
- ### Dockerfile design
421
-
422
- See the [standard Dockerfile template](src/common_python_tasks/data/generic/Dockerfile.j2) for the implementation.
423
-
424
- - Multi-stage build: The build stage installs uv and builds a wheel. The runtime stage installs only the wheel to keep the final image slim and reproducible.
425
- - Cache mounts: Pip and uv cache mounts speed up iterative builds without bloating the final image.
426
- - Explicit build metadata: `PYTHON_VERSION`, `UV_VERSION`, `PACKAGE_VERSION`, `PACKAGE_NAME`, `AUTHORS`, and `GIT_COMMIT` make image metadata predictable and auditable.
427
- - Project-level build settings: `CONTAINER_APT_PACKAGES`, `CONTAINER_CUSTOM_ENTRYPOINT`, and `CONTAINER_DEPS_IMAGE` configure generated image behavior without being encoded as generic Docker arguments.
428
- - Optional debug stage: The image exports and installs the `debug` dependency group only when present and does not include it in the default final image.
429
- - Stable package path: Symlinks give entrypoints and consumers consistent `/pkg` and `/_$PACKAGE_NAME` paths regardless of wheel layout.
430
- - Safe entrypoint selection: The default entrypoint resolves the console script matching the package name and falls back to `python`. `CONTAINER_CUSTOM_ENTRYPOINT` is validated against `[project].scripts`.
431
- - Minimal final image: The standard slim Python base, cache cleanup, and explicit `runtime` final target keep the default image small.
432
-
433
- ## Project notes
434
-
435
- - This project dogfoods itself. Set `PYTHONPATH=src` when running its tasks locally so Poe uses the local package rather than the installed version.
436
- - `RELEASE_UPDATE_CHANGELOG` is enabled by default and prepends the generated changelog section before the release tag is created. Set it to a falsy value to manage changelog commits yourself.
437
- - `RELEASE_PRE_SCRIPT` and `RELEASE_POST_SCRIPT` are advanced hooks for release-specific work, such as updating a version reference in another file.
438
- - The project is in alpha status, so breaking changes may occur between minor versions before 1.0.0.
439
-
440
- ## Contributing
441
-
442
- Contributions and feedback are welcome. Please open an issue or discussion to talk through a change before submitting a pull request.