common-python-tasks 0.4.1__py3-none-any.whl → 0.5.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.
@@ -186,11 +186,12 @@ def split_delimited_values(
186
186
  separators: str = ":",
187
187
  allow_whitespace: bool = False,
188
188
  ) -> list[str]:
189
- """Split a delimited string while preserving quoted substrings.
189
+ """Split a delimited string while preserving quoted or escaped separators.
190
190
 
191
191
  Args:
192
192
  value: The string to split.
193
- separators: Explicit separator characters to use.
193
+ separators: Explicit separator characters to use. Prefix a separator with
194
+ a backslash to include it literally in a token.
194
195
  allow_whitespace: Treat whitespace as additional separators.
195
196
 
196
197
  Returns:
@@ -207,7 +208,7 @@ def split_delimited_values(
207
208
 
208
209
 
209
210
  def split_colon_delimited_values(value: str) -> list[str]:
210
- """Split a colon- or whitespace-delimited string while preserving quoted substrings."""
211
+ """Split colon-delimited values while preserving quoted or escaped colons."""
211
212
  return split_delimited_values(value, separators=":", allow_whitespace=True)
212
213
 
213
214
 
@@ -221,7 +222,7 @@ def parse_container_build_args(
221
222
  env_build_args: str | None,
222
223
  ) -> dict[str, str] | None:
223
224
  """Parse container build args from CLI or environment input."""
224
- if cli_build_args is not None:
225
+ if cli_build_args and any(token.strip() for token in cli_build_args):
225
226
  build_arg_tokens = [
226
227
  token.strip() for token in cli_build_args if token and token.strip()
227
228
  ]
@@ -233,10 +234,15 @@ def parse_container_build_args(
233
234
  parsed_build_args: dict[str, str] = {}
234
235
  for token in build_arg_tokens:
235
236
  if "=" not in token:
236
- LOGGER.warning("Ignoring invalid build-arg token: %s", token)
237
- continue
237
+ utils.fatal(
238
+ f"Invalid build-arg token {token!r}; expected KEY=VALUE. "
239
+ "Escape literal colons in values as \\: or quote the value."
240
+ )
238
241
  key, value = token.split("=", 1)
239
- parsed_build_args[key.strip()] = value.strip()
242
+ key = key.strip()
243
+ if not key:
244
+ utils.fatal(f"Invalid build-arg token {token!r}; the key must not be empty")
245
+ parsed_build_args[key] = value.strip()
240
246
 
241
247
  return parsed_build_args or None
242
248
 
@@ -254,13 +260,20 @@ def parse_container_env_tokens(value: str | list[str] | None) -> list[str]:
254
260
  return []
255
261
 
256
262
  if isinstance(value, list):
257
- return [token.strip() for token in value if token and token.strip()]
258
-
259
- if not value.strip():
263
+ tokens = [token.strip() for token in value if token and token.strip()]
264
+ elif not value.strip():
260
265
  return []
266
+ else:
267
+ tokens = split_colon_delimited_values(value)
261
268
 
262
- tokens = split_colon_delimited_values(value)
263
- return [token for token in tokens if token.strip()]
269
+ for token in tokens:
270
+ if "=" not in token or not token.split("=", 1)[0].strip():
271
+ utils.fatal(
272
+ f"Invalid container env token {token!r}; expected KEY=VALUE. "
273
+ "Escape literal colons in values as \\: or quote the value."
274
+ )
275
+
276
+ return tokens
264
277
 
265
278
 
266
279
  def _parse_container_envfile_paths(
@@ -3,6 +3,8 @@ import logging
3
3
  import mimetypes
4
4
  import os
5
5
  import re
6
+ from collections.abc import Callable
7
+ from functools import lru_cache
6
8
  from pathlib import Path
7
9
  from typing import Any, Literal
8
10
 
@@ -39,7 +41,7 @@ def should_publish_github_release() -> bool:
39
41
  """Return whether GitHub Releases publication is enabled.
40
42
 
41
43
  Returns:
42
- True if GitHub Release publishing is enabled, False if it is skipped.
44
+ `True` if GitHub Release publishing is enabled, `False` if it is skipped.
43
45
  """
44
46
  return not env_truthy("SKIP_GITHUB_RELEASE")
45
47
 
@@ -51,13 +53,25 @@ def get_github_repository() -> str | None:
51
53
  The GitHub repository slug in the form `owner/repo`, or `None` if it
52
54
  cannot be determined.
53
55
  """
54
- repository = os.getenv("GITHUB_REPOSITORY", "").strip()
56
+ return _get_github_repository(
57
+ os.getenv("GITHUB_REPOSITORY", "").strip(),
58
+ Path.cwd(),
59
+ utils.run_command,
60
+ )
61
+
62
+
63
+ @lru_cache(maxsize=32)
64
+ def _get_github_repository(
65
+ repository: str,
66
+ _working_directory: Path,
67
+ run_command: Callable[..., Any],
68
+ ) -> str | None:
55
69
  if repository:
56
70
  if re.fullmatch(r"[^/]+/[^/]+", repository):
57
71
  return repository
58
72
  LOGGER.warning("Ignoring invalid GITHUB_REPOSITORY value: %s", repository)
59
73
 
60
- result = utils.run_command(
74
+ result = run_command(
61
75
  ["git", "remote", "get-url", "origin"],
62
76
  capture_output=True,
63
77
  acceptable_returncodes={0, 128},
@@ -94,8 +108,13 @@ def get_github_token() -> str | None:
94
108
  if token:
95
109
  return token
96
110
 
111
+ return _get_github_cli_token(utils.run_command)
112
+
113
+
114
+ @lru_cache(maxsize=8)
115
+ def _get_github_cli_token(run_command: Callable[..., Any]) -> str | None:
97
116
  try:
98
- result = utils.run_command(
117
+ result = run_command(
99
118
  ["gh", "auth", "token"],
100
119
  capture_output=True,
101
120
  acceptable_returncodes={0, 1},
@@ -132,13 +151,18 @@ def get_github_api_base_url() -> str:
132
151
  The base URL for GitHub API requests, defaulting to GitHub.com API or
133
152
  GitHub Enterprise API if configured.
134
153
  """
135
- api_url = os.getenv("GITHUB_API_URL", "").strip()
154
+ return _get_github_api_base_url(
155
+ os.getenv("GITHUB_API_URL", "").strip(),
156
+ os.getenv("GITHUB_SERVER_URL", "https://github.com").strip(),
157
+ )
158
+
159
+
160
+ @lru_cache(maxsize=32)
161
+ def _get_github_api_base_url(api_url: str, server_url: str) -> str:
136
162
  if api_url:
137
163
  return api_url.rstrip("/")
138
164
 
139
- server_url = (
140
- os.getenv("GITHUB_SERVER_URL", "https://github.com").strip().rstrip("/")
141
- )
165
+ server_url = server_url.rstrip("/")
142
166
  if server_url.endswith("github.com"):
143
167
  return "https://api.github.com"
144
168
  return f"{server_url}/api/v3"
@@ -156,7 +180,11 @@ def get_github_upload_api_base_url(api_base_url: str | None = None) -> str:
156
180
  Returns:
157
181
  The upload base URL for GitHub API requests.
158
182
  """
159
- api_base = api_base_url or get_github_api_base_url()
183
+ return _get_github_upload_api_base_url(api_base_url or get_github_api_base_url())
184
+
185
+
186
+ @lru_cache(maxsize=32)
187
+ def _get_github_upload_api_base_url(api_base: str) -> str:
160
188
  if api_base == "https://api.github.com":
161
189
  return "https://uploads.github.com"
162
190
  return api_base
@@ -447,7 +475,8 @@ def get_github_release_asset_paths(
447
475
  asset_sources: Optional list of paths or glob patterns. When a string is
448
476
  provided (including via `GITHUB_RELEASE_ASSETS`), values are
449
477
  interpreted as colon-separated paths or glob patterns.
450
- If not provided, the default value is `dist/*`.
478
+ If not provided, the default value is `dist/*`. An empty list
479
+ explicitly disables asset selection.
451
480
 
452
481
  Returns:
453
482
  A sorted list of existing asset paths.
@@ -485,7 +514,7 @@ def get_github_release_asset_paths(
485
514
  elif path.exists():
486
515
  LOGGER.warning("Ignoring non-file GitHub release asset path: %s", path)
487
516
 
488
- if not asset_paths and explicit_assets:
517
+ if not asset_paths and explicit_assets and source_items:
489
518
  utils.fatal(
490
519
  "No GitHub Release assets were found for the configured asset paths: "
491
520
  + ":".join(source_items)
@@ -18,6 +18,11 @@ from . import utils
18
18
  LOGGER = logging.getLogger(__name__)
19
19
 
20
20
  PACKAGE_MANAGER_ENV_VAR = "COMMON_PYTHON_TASKS_PACKAGE_MANAGER"
21
+ PACKAGE_MANAGER_ENV_VARS = (
22
+ PACKAGE_MANAGER_ENV_VAR,
23
+ "COMMON_PYTHON_TASKS_BUILD_TOOL",
24
+ "PACKAGE_MANAGER",
25
+ )
21
26
 
22
27
 
23
28
  class PackageManager(StrEnum):
@@ -45,9 +50,10 @@ def _parse_tool_version_output(output: str, tool_name: str) -> str:
45
50
 
46
51
 
47
52
  def _find_package_manager_override() -> tuple[str, str] | None:
48
- value = os.getenv(PACKAGE_MANAGER_ENV_VAR)
49
- if value is not None and value.strip():
50
- return PACKAGE_MANAGER_ENV_VAR, value.strip().lower()
53
+ for env_var in PACKAGE_MANAGER_ENV_VARS:
54
+ value = os.getenv(env_var)
55
+ if value is not None and value.strip():
56
+ return env_var, value.strip().lower()
51
57
  return None
52
58
 
53
59
 
@@ -105,7 +111,8 @@ def get_package_manager() -> PackageManager:
105
111
  """Resolve the package manager backend used for build and publish commands.
106
112
 
107
113
  Resolution order:
108
- 1. Environment override via `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`.
114
+ 1. Environment overrides via `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`,
115
+ `COMMON_PYTHON_TASKS_BUILD_TOOL`, or `PACKAGE_MANAGER`, in that order.
109
116
  2. Auto-discovery from installed executables and project metadata.
110
117
 
111
118
  Supported values are `poetry`, `uv`, and `auto`.
@@ -374,7 +381,13 @@ def read_pyproject_toml() -> dict[str, Any]:
374
381
  return tomllib.loads(Path("pyproject.toml").read_text())
375
382
 
376
383
 
377
- BLACK_TARGET_PYTHON_CANDIDATES = [Version(f"3.{minor}") for minor in range(7, 17)]
384
+ BLACK_TARGET_PYTHON_MINORS = range(7, 17)
385
+
386
+
387
+ def _specifier_supports_python_minor(specifier_set: SpecifierSet, minor: int) -> bool:
388
+ return any(
389
+ specifier_set.contains(Version(f"3.{minor}.{patch}")) for patch in range(1000)
390
+ )
378
391
 
379
392
 
380
393
  def get_black_target_version() -> str | None:
@@ -394,9 +407,9 @@ def get_black_target_version() -> str | None:
394
407
  LOGGER.warning("Unable to parse requires-python specifier: %s", requires_python)
395
408
  return None
396
409
 
397
- for candidate in BLACK_TARGET_PYTHON_CANDIDATES:
398
- if specifier_set.contains(candidate):
399
- return f"py{candidate.major}{candidate.minor:02d}"
410
+ for minor in BLACK_TARGET_PYTHON_MINORS:
411
+ if _specifier_supports_python_minor(specifier_set, minor):
412
+ return f"py3{minor:02d}"
400
413
  return None
401
414
 
402
415
 
@@ -413,22 +426,57 @@ def has_debug_dependency_group() -> bool:
413
426
  return bool(debug_group)
414
427
 
415
428
 
416
- def resolve_container_entrypoint_command(custom_entrypoint: str | None = None) -> str:
429
+ def _get_project_scripts() -> dict[str, str]:
430
+ scripts = read_pyproject_toml().get("project", {}).get("scripts")
431
+ if not isinstance(scripts, dict):
432
+ return {}
433
+
434
+ normalized_scripts = {
435
+ str(script_name).strip(): str(script_target)
436
+ for script_name, script_target in scripts.items()
437
+ if str(script_name).strip()
438
+ }
439
+ return normalized_scripts
440
+
441
+
442
+ def _get_available_container_entrypoint_scripts() -> tuple[str, ...]:
443
+ return tuple(sorted(_get_project_scripts().keys()))
444
+
445
+
446
+ def resolve_container_entrypoint_command(entrypoint_script: str | None = None) -> str:
417
447
  """Resolve the command used in the generated container entrypoint script.
418
448
 
419
449
  Args:
420
- custom_entrypoint: Optional explicit command override.
450
+ entrypoint_script: Optional explicit entrypoint script override.
421
451
 
422
452
  Returns:
423
453
  A command name to place in `/pkg/entrypoint.sh`.
424
454
  """
425
455
 
426
- if custom_entrypoint is not None and custom_entrypoint.strip():
427
- return custom_entrypoint.strip()
456
+ available_scripts = _get_available_container_entrypoint_scripts()
457
+ scripts_set = set(available_scripts)
458
+
459
+ if entrypoint_script is not None and entrypoint_script.strip():
460
+ selected_entrypoint = entrypoint_script.strip()
461
+ if selected_entrypoint in scripts_set:
462
+ return selected_entrypoint
463
+
464
+ if available_scripts:
465
+ utils.fatal(
466
+ "Invalid CUSTOM_ENTRYPOINT value "
467
+ f"{selected_entrypoint!r}. "
468
+ "It must match a key in [project].scripts. "
469
+ f"Available scripts: {', '.join(available_scripts)}"
470
+ )
471
+
472
+ utils.fatal(
473
+ "Invalid CUSTOM_ENTRYPOINT value "
474
+ f"{selected_entrypoint!r}. "
475
+ "No [project].scripts entries were found in pyproject.toml."
476
+ )
428
477
 
429
478
  script_name = utils.get_package_name(use_underscores=True)
430
- scripts = read_pyproject_toml().get("project", {}).get("scripts")
431
- if isinstance(scripts, dict) and script_name in scripts:
479
+ if script_name in scripts_set:
432
480
  return script_name
433
481
 
434
482
  return "python"
@@ -582,7 +582,7 @@ def build_image(
582
582
 
583
583
  apt_packages = (parsed_build_args or {}).get("APT_PACKAGES", "").strip()
584
584
  entrypoint_command = resolve_container_entrypoint_command(
585
- (parsed_build_args or {}).get("CUSTOM_ENTRYPOINT")
585
+ entrypoint_script=(parsed_build_args or {}).get("CUSTOM_ENTRYPOINT")
586
586
  )
587
587
  container_env_vars = load_container_env_tokens(
588
588
  container_env,
@@ -884,7 +884,7 @@ def run_container(
884
884
  f"No local images found for {package_name}. Build the image first or specify a tag."
885
885
  )
886
886
 
887
- LOGGER.info("Running container %s", selected_image)
887
+ LOGGER.debug("Running container %s", selected_image)
888
888
  run_args = ["docker", "run", "--rm", "-i", "-t"]
889
889
  if privileged:
890
890
  run_args.append("--privileged")
@@ -1000,7 +1000,8 @@ def publish_github_release(
1000
1000
  release_name or os.getenv("GITHUB_RELEASE_NAME") or resolved_tag_name
1001
1001
  )
1002
1002
  resolved_body = body or os.getenv("GITHUB_RELEASE_BODY")
1003
- asset_paths = get_github_release_asset_paths(assets)
1003
+ # Poe supplies `[]` when an optional repeated CLI argument is omitted.
1004
+ asset_paths = get_github_release_asset_paths(assets or None)
1004
1005
 
1005
1006
  return publish_github_release_helper(
1006
1007
  resolved_tag_name,
@@ -1088,6 +1089,26 @@ def changelog() -> None:
1088
1089
  print(_changelog())
1089
1090
 
1090
1091
 
1092
+ def _build_and_publish_release_package(release_tag: str) -> None:
1093
+ from .utils import run_command
1094
+
1095
+ package_published = False
1096
+ try:
1097
+ build_package(clean_dist=True)
1098
+ publish_package(build_first=False)
1099
+ package_published = True
1100
+ finally:
1101
+ if not package_published:
1102
+ LOGGER.warning(
1103
+ "Package build or publication failed; deleting local release tag %s",
1104
+ release_tag,
1105
+ )
1106
+ run_command(
1107
+ ["git", "tag", "--delete", release_tag],
1108
+ acceptable_returncodes={0, 1},
1109
+ )
1110
+
1111
+
1091
1112
  def _run_release_flow(
1092
1113
  component: str = "auto",
1093
1114
  stage: str | None = None,
@@ -1167,8 +1188,7 @@ def _run_release_flow(
1167
1188
  run_command(["sh", "-lc", post_script], env=hook_env, dry_run=False)
1168
1189
  return
1169
1190
 
1170
- build_package(clean_dist=True)
1171
- publish_package(build_first=False)
1191
+ _build_and_publish_release_package(hook_env["RELEASE_TAG"])
1172
1192
 
1173
1193
  if include_containers:
1174
1194
  build_image(
@@ -1410,6 +1430,7 @@ def fastapi_stack_up(
1410
1430
  from .env import (
1411
1431
  get_cache_id_suffix,
1412
1432
  load_container_env_tokens,
1433
+ parse_container_build_args,
1413
1434
  )
1414
1435
  from .project import (
1415
1436
  get_package_manager,
@@ -1429,6 +1450,13 @@ def fastapi_stack_up(
1429
1450
  )
1430
1451
 
1431
1452
  container_env_vars = load_container_env_tokens(container_env, container_envfile)
1453
+ parsed_build_args = parse_container_build_args(
1454
+ build_args,
1455
+ os.getenv("CONTAINER_BUILD_ARGS"),
1456
+ )
1457
+ entrypoint_command = resolve_container_entrypoint_command(
1458
+ entrypoint_script=(parsed_build_args or {}).get("CUSTOM_ENTRYPOINT")
1459
+ )
1432
1460
 
1433
1461
  cache_id_suffix = get_cache_id_suffix(no_cache)
1434
1462
 
@@ -1439,7 +1467,7 @@ def fastapi_stack_up(
1439
1467
  "EXTENSION_CONTENT": "",
1440
1468
  "HAS_DEBUG_DEPS": has_debug_deps,
1441
1469
  "APT_PACKAGES": "",
1442
- "ENTRYPOINT_COMMAND": resolve_container_entrypoint_command(),
1470
+ "ENTRYPOINT_COMMAND": entrypoint_command,
1443
1471
  "CONTAINER_ENV_VARS": container_env_vars,
1444
1472
  "CACHE_ID_SUFFIX": cache_id_suffix,
1445
1473
  },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: common-python-tasks
3
- Version: 0.4.1
3
+ Version: 0.5.1
4
4
  Summary: Opinionated Poe the Poet tasks for Python package development.
5
5
  Project-URL: Homepage, http://github.com/ci-sourcerer/common-python-tasks
6
6
  Project-URL: Source, http://github.com/ci-sourcerer/common-python-tasks.git
@@ -46,14 +46,16 @@ Instead of writing your own tasks for formatting, linting, testing, packaging, a
46
46
  You can add `common-python-tasks` to a new project by using the handy automated installation script.
47
47
 
48
48
  ```shell
49
- curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/add-common-python-tasks.sh | jq -r '.content' | base64 -d | sh
49
+ curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/add_common_python_tasks.py | jq -r '.content' | base64 -d | python3
50
50
  ```
51
51
 
52
52
  To install a specific release, set the environment variable `COMMON_PYTHON_TASKS_VERSION`.
53
53
 
54
54
  ```shell
55
- COMMON_PYTHON_TASKS_VERSION=0.4.1 \
56
- sh -c "$(curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/add-common-python-tasks.sh | jq -r '.content' | base64 -d)"
55
+ curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/add_common_python_tasks.py \
56
+ | jq -r '.content' \
57
+ | base64 -d \
58
+ | COMMON_PYTHON_TASKS_VERSION=0.5.1 python3
57
59
  ```
58
60
 
59
61
  This will complete the following steps.
@@ -73,7 +75,7 @@ There's no real reason to run the automated script; I just like automating every
73
75
  ```toml
74
76
  [project]
75
77
  dependencies = [
76
- "common-python-tasks==0.4.1" # Always pin to a specific version
78
+ "common-python-tasks==0.5.1" # Always pin to a specific version
77
79
  ]
78
80
 
79
81
  [tool.poe]
@@ -220,7 +222,7 @@ Supported values are `poetry`, `uv`, and `auto`.
220
222
 
221
223
  Tasks that need configuration files (`pytest`, `coverage`, `flake8`, `isort`) follow this order of precedence.
222
224
 
223
- 1. **`pyproject.toml` sections**: `[tool.pytest]`, `[tool.coverage]`, `[tool.isort]` take priority
225
+ 1. **`pyproject.toml` sections**: `[tool.pytest.ini_options]`, `[tool.coverage]`, `[tool.isort]` take priority
224
226
  2. **Environment variables**: Override config paths; see [Environment Variables](#environment-variables)
225
227
  3. **Local config files**: `pytest.ini`, `.coveragerc`, `.flake8`, `.isort.cfg` in the project root
226
228
  4. **Bundled defaults**: Sensible defaults included with this package, found in the [`src/common_python_tasks/data`](src/common_python_tasks/data) directory
@@ -248,19 +250,19 @@ The following environment variables configure package and container behavior.
248
250
  - `PACKAGE_MANAGER`: Alias for `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`
249
251
  - `CONTAINER_REGISTRY_USERNAME`: Specifies the container registry username for image tagging; the default is the current local user
250
252
  - `CONTAINER_REGISTRY_URL`: Specifies the registry URL (default: `docker.io/{username}`)
251
- - `CONTAINER_BUILD_ARGS`: Provides additional Docker build arguments in `KEY=VALUE:OTHER=VALUE` format
252
- - `CONTAINER_EXTENSION_FILES`: Specifies colon delimited local extension Dockerfile paths to include in the rendered build
253
- - `CONTAINER_EXTENSIONS`: Specifies colon delimited extension bundle names or parameterized bundle values to include in the rendered build
254
- - `CONTAINER_ENV`: Provides colon delimited `KEY=VALUE` declarations to inject into the builder stage of the rendered Dockerfile
253
+ - `CONTAINER_BUILD_ARGS`: Provides additional Docker build arguments in `KEY=VALUE:OTHER=VALUE` format. Escape literal colons in values as `\:` or quote the complete value
254
+ - `CONTAINER_EXTENSION_FILES`: Specifies colon delimited local extension Dockerfile paths to include in the rendered build; escape literal colons as `\:` or quote the complete path
255
+ - `CONTAINER_EXTENSIONS`: Specifies colon delimited extension bundle names or parameterized bundle values to include in the rendered build; escape literal colons as `\:` or quote the complete value
256
+ - `CONTAINER_ENV`: Provides colon delimited `KEY=VALUE` declarations to inject into the builder stage of the rendered Dockerfile. Escape literal colons in values as `\:` or quote the complete value
255
257
  - `.containerenv`: Can also supply the same declarations from a file in the project root. It is loaded first, then `container_envfile`, then `CONTAINER_ENV`, and finally `container_env`.
256
258
  - `CONTAINER_PRUNE_KEEP`: Controls image pruning after builds (`-1` keeps all, `0` keeps the latest only, `N` keeps the latest plus `N` previous)
257
- - `CUSTOM_ENTRYPOINT`: Specifies a custom entrypoint script name for containers
259
+ - `CUSTOM_ENTRYPOINT`: Specifies a custom entrypoint script name for containers. The value must match a key in `[project].scripts`; unknown values fail the build
258
260
  - `CONTAINER_DEPS_CONTENT`: Supplies inline Dockerfile instructions for a dependency image that installs artifacts into `/tmp/deps`
259
- - `CONTAINER_DEPS_FILE`: Points to one or more explicit Dockerfiles to build the dependency image. It may be a colon delimited list of file paths and is used only when `CONTAINER_DEPS_CONTENT` is unset
261
+ - `CONTAINER_DEPS_FILE`: Points to one or more explicit Dockerfiles to build the dependency image. It may be a colon delimited list of file paths, with literal colons escaped as `\:`, and is used only when `CONTAINER_DEPS_CONTENT` is unset
260
262
  - `CONTAINER_DEPS_MAPPINGS`: Maps copied dependency names from `/tmp/deps` into destination paths as space separated `name:/target/path` entries. This is used only when `CONTAINER_DEPS_MOVE_SCRIPT` or `CONTAINER_DEPS_MOVE_SCRIPT_PATH` is not set
261
263
  - `CONTAINER_DEPS_MOVE_SCRIPT`: Supplies a raw executable script to run after `/tmp/deps` is copied into the image. The script is written to `/tmp/container-deps-move-script` and executed with its own shebang
262
264
  - `CONTAINER_DEPS_MOVE_SCRIPT_PATH`: Supplies a host path to a script file to run after `/tmp/deps` is copied into the image. This path takes precedence over `CONTAINER_DEPS_MOVE_SCRIPT` when both are set
263
- - `GITHUB_RELEASE_ASSETS`: Colon separated list of file paths or glob patterns to attach to the GitHub Release (default: `dist/*`)
265
+ - `GITHUB_RELEASE_ASSETS`: Colon separated list of file paths or glob patterns to attach to the GitHub Release (default: `dist/*`); escape literal colons as `\:` or quote the complete path
264
266
  - `SKIP_GITHUB_RELEASE`: Truthy value to skip GitHub Release publication
265
267
  - `GITHUB_TOKEN` or `GH_TOKEN`: GitHub authentication token used to publish releases and upload assets
266
268
  - `GITHUB_REPOSITORY`: Optional override for the repository slug used when publishing a GitHub Release
@@ -307,8 +309,8 @@ By default, `tasks()` exposes the common task set. You can still include or excl
307
309
  ```toml
308
310
  [project]
309
311
  name = "simple-cli-tool"
310
- version = "0.4.1"
311
- dependencies = ["common-python-tasks==0.4.1"]
312
+ version = "0.5.1"
313
+ dependencies = ["common-python-tasks==0.5.1"]
312
314
 
313
315
  [tool.poe]
314
316
  include_script = "common_python_tasks:tasks()"
@@ -320,7 +322,7 @@ Available tasks: common defaults such as `format`, `lint`, `test`, and `build`.
320
322
 
321
323
  ```toml
322
324
  [project]
323
- dependencies = ["common-python-tasks==0.4.1"]
325
+ dependencies = ["common-python-tasks==0.5.1"]
324
326
 
325
327
  [tool.poe]
326
328
  include_script = "common_python_tasks:tasks(include_tags=['common', 'containers'])"
@@ -336,7 +338,7 @@ Available tasks: All tasks including `build-image` and `push-image`.
336
338
 
337
339
  ```toml
338
340
  [project]
339
- dependencies = ["common-python-tasks==0.4.1"]
341
+ dependencies = ["common-python-tasks==0.5.1"]
340
342
 
341
343
  [tool.poe]
342
344
  include_script = "common_python_tasks:tasks()"
@@ -424,7 +426,7 @@ The standard Python Dockerfile incorporates several intentional design choices.
424
426
  - Explicit inputs through build args: `PYTHON_VERSION`, `PACKAGE_MANAGER`, `PACKAGE_MANAGER_VERSION`, `PACKAGE_NAME`, `AUTHORS`, `GIT_COMMIT`, and `CUSTOM_ENTRYPOINT` make image metadata and behavior predictable and auditable
425
427
  - Optional debug stage: Exports and installs the `debug` dependency group only when present, without failing otherwise, and is not part of the default final image
426
428
  - Stable package path: Creates symlinks to the installed package so entrypoints and consumers have a consistent `/pkg` and `/_$PACKAGE_NAME` path regardless of wheel layout. This ensures that the package can be imported and executed from a known location and supports the less common case of reading files directly from the package path
427
- - Safe entrypoint selection: The default entrypoint resolves the console script matching the package name, while `CUSTOM_ENTRYPOINT` allows an override at build time and keeps runtime behavior predictable
429
+ - Safe entrypoint selection: The default entrypoint resolves the console script matching the package name and falls back to `python` when no matching script exists. `CUSTOM_ENTRYPOINT` allows an override at build time, and the override is validated strictly against `[project].scripts`
428
430
  - Minimal final image: Uses the slim Python base by default, cleans wheel artifacts and caches, and sets `runtime` as the explicit final target so the debug stage remains optional
429
431
 
430
432
  ## Notes
@@ -2,11 +2,11 @@ common_python_tasks/__init__.py,sha256=A-qMF818PGbDxM_DH9l1HwQMV-tXn8CZZiVQy-F_s
2
2
  common_python_tasks/__main__.py,sha256=AZiLLusMDs0rZIIBuCDbt6jABlQD-kEwIQIep7GxgEc,3019
3
3
  common_python_tasks/docker.py,sha256=KQ_Ym6tungnYqkf1EyXgdLpMjLNX879SWXKM9tIrfEg,19694
4
4
  common_python_tasks/docker_compose.py,sha256=UnTP5-Kw31ZzkMUeuml1GxHkHmNsX76-9uezq0qPVBQ,21057
5
- common_python_tasks/env.py,sha256=DNz-qpFnJ-41pHBb7MIJuemgLqn_rI3GUEZTfC2I4bQ,15064
5
+ common_python_tasks/env.py,sha256=X-v2gX6SJluvkjU7M941UbrMZ73vHnxwLeaBWRUxeLc,15678
6
6
  common_python_tasks/git.py,sha256=wJ3gePpWE4fhNJYZCEWTDFrhHqaOVkaQveHeiGIbCHo,11188
7
- common_python_tasks/github.py,sha256=sABDB7BvrdWkpbh4u0-8OeAiu-jWUYfjre_nKOw15bI,18393
8
- common_python_tasks/project.py,sha256=zyhs1oqptxRALPCzJtNOsQJBeNEbWzKIWBG0BUoiXLg,19271
9
- common_python_tasks/tasks.py,sha256=P-T6UMhOyhOfOTG2M9EGiK4YfTm9l5mGplF6SnLt1vo,53926
7
+ common_python_tasks/github.py,sha256=dLlHf6MYIrs0iTlBXtGm4yYgkzCZ0kJj4265ChH1O0U,19170
8
+ common_python_tasks/project.py,sha256=5ZU1PxefUPaSjPEGhEW3kiBLGBgt6O0VGejmVEg486E,20769
9
+ common_python_tasks/tasks.py,sha256=UIf6OX5C0MvZbIPTsLfR0Tr47TBtaiGLsJKbezabMq4,54906
10
10
  common_python_tasks/utils.py,sha256=ej4nKFPMFhu1wSgkL9x50DML_sPoPmR6aGcIMyAmfN8,19586
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=IVXmnF1pJvW3o8dXBLIVhmng7DyXu_sWXx_W3VlAcGE,910
@@ -22,7 +22,7 @@ common_python_tasks/data/generic/Dockerfile.deps.j2,sha256=rCGsBwLHQIRsGFzDbiRi_
22
22
  common_python_tasks/data/generic/Dockerfile.j2,sha256=UG-QxIuFT27j925LsghPl5Wqcw5wNsPPuqB6rcRngz0,7263
23
23
  common_python_tasks/data/generic/cliff.toml,sha256=po1MnEGEEgrpyBz3XLWr0h7emXb6ASX63lrU2r6o240,77
24
24
  common_python_tasks/data/generic/pytest.ini,sha256=RHak5QrLzKHvUBjFmaaiOYFrd_8YBRQubXEOT0NJfco,134
25
- common_python_tasks-0.4.1.dist-info/METADATA,sha256=p2tiuAPYuNHa8wx_m9NcgkEbfwQYO9lRY-ox71rOM8g,22054
26
- common_python_tasks-0.4.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
27
- common_python_tasks-0.4.1.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
28
- common_python_tasks-0.4.1.dist-info/RECORD,,
25
+ common_python_tasks-0.5.1.dist-info/METADATA,sha256=rfvEbPvkxBa35iWaLL0KfBDoIFWPjMpTOgjBbHfVTFE,22592
26
+ common_python_tasks-0.5.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
27
+ common_python_tasks-0.5.1.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
28
+ common_python_tasks-0.5.1.dist-info/RECORD,,