common-python-tasks 0.7.0__tar.gz → 0.8.0__tar.gz

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.
Files changed (38) hide show
  1. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/.gitignore +2 -0
  2. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/PKG-INFO +38 -12
  3. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/README.md +36 -10
  4. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/pyproject.toml +6 -2
  5. common_python_tasks-0.8.0/scripts/readme_tasks_table.py +78 -0
  6. common_python_tasks-0.8.0/scripts/release_script.py +102 -0
  7. common_python_tasks-0.8.0/scripts/update_readme_tasks_table.py +37 -0
  8. common_python_tasks-0.8.0/scripts/utils.py +76 -0
  9. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/__init__.py +9 -0
  10. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/__main__.py +19 -3
  11. common_python_tasks-0.8.0/src/common_python_tasks/data/dockerfile_extensions/.gitkeep +0 -0
  12. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/Dockerfile.j2 +5 -5
  13. common_python_tasks-0.8.0/src/common_python_tasks/data/generic/ruff.toml +2 -0
  14. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/docker.py +294 -154
  15. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/docker_compose.py +12 -4
  16. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/env.py +82 -36
  17. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/git.py +30 -16
  18. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/github.py +17 -2
  19. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/project.py +2 -2
  20. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/tasks.py +138 -157
  21. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/utils.py +29 -4
  22. common_python_tasks-0.7.0/scripts/release_script.py +0 -214
  23. common_python_tasks-0.7.0/src/common_python_tasks/data/generic/ruff.toml +0 -6
  24. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/.poetry/plugins/poetry_dynamic_versioning-1.10.0.dist-info/licenses/LICENSE +0 -0
  25. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/.poetry/plugins/poetry_plugin_export-1.10.0.dist-info/licenses/LICENSE +0 -0
  26. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/LICENSE +0 -0
  27. /common_python_tasks-0.7.0/src/common_python_tasks/data/dockerfile_extensions/.gitkeep → /common_python_tasks-0.8.0/scripts/__init__.py +0 -0
  28. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/scripts/add_common_python_tasks.py +0 -0
  29. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/alembic.ini.j2 +0 -0
  30. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/compose-base.yml.j2 +0 -0
  31. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/compose-db-debug.yml.j2 +0 -0
  32. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/compose-db.yml.j2 +0 -0
  33. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/compose-debug.yml.j2 +0 -0
  34. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/.coveragerc +0 -0
  35. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/.dockerignore +0 -0
  36. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/Dockerfile.deps.j2 +0 -0
  37. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/cliff.toml +0 -0
  38. {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/pytest.ini +0 -0
@@ -214,3 +214,5 @@ __marimo__/
214
214
 
215
215
  # Streamlit
216
216
  .streamlit/secrets.toml
217
+
218
+ .serena
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: common-python-tasks
3
- Version: 0.7.0
3
+ Version: 0.8.0
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
@@ -43,9 +43,9 @@ It supplies sensible defaults for formatting, linting, testing, packaging, relea
43
43
  Add `common-python-tasks` as a development dependency from your project root.
44
44
 
45
45
  ```shell
46
- uv add --dev common-python-tasks==0.7.0
46
+ uv add --dev common-python-tasks==0.8.0
47
47
  # or, for Poetry projects
48
- poetry add --group dev common-python-tasks==0.7.0
48
+ poetry add --group dev common-python-tasks==0.8.0
49
49
  ```
50
50
 
51
51
  Configure Poe the Poet to expose the default `common` task set.
@@ -70,7 +70,7 @@ The helper script performs the same development-dependency installation and Poe
70
70
  ```shell
71
71
  curl --fail --silent --show-error --location \
72
72
  --output /tmp/add_common_python_tasks.py \
73
- https://raw.githubusercontent.com/ci-sourcerer/common-python-tasks/v0.7.0/scripts/add_common_python_tasks.py
73
+ https://raw.githubusercontent.com/ci-sourcerer/common-python-tasks/v0.8.0/scripts/add_common_python_tasks.py
74
74
  ```
75
75
 
76
76
  Review the downloaded script before executing it.
@@ -82,10 +82,10 @@ less /tmp/add_common_python_tasks.py
82
82
  Run the reviewed script with the same pinned package version.
83
83
 
84
84
  ```shell
85
- COMMON_PYTHON_TASKS_VERSION=0.7.0 python3 /tmp/add_common_python_tasks.py
85
+ COMMON_PYTHON_TASKS_VERSION=0.8.0 python3 /tmp/add_common_python_tasks.py
86
86
  ```
87
87
 
88
- To install another release, replace both occurrences of `0.7.0` with that release's version.
88
+ To install another release, replace both occurrences of `0.8.0` with that release's version.
89
89
 
90
90
  ## Available tasks
91
91
 
@@ -196,7 +196,7 @@ After installing the package, a minimal project configuration looks like this.
196
196
  ```toml
197
197
  [project]
198
198
  name = "simple-cli-tool"
199
- version = "0.7.0"
199
+ version = "0.8.0"
200
200
 
201
201
  [tool.poe]
202
202
  include_script = "common_python_tasks:tasks()"
@@ -240,13 +240,16 @@ addopts = "-ra"
240
240
 
241
241
  - `CONTAINER_REGISTRY_USERNAME`: Container-registry username for image tagging; the default is the current local user
242
242
  - `CONTAINER_REGISTRY_URL`: Registry URL with a default of `docker.io/{username}`
243
- - `CONTAINER_BUILD_ARGS`: Additional Docker build arguments in `KEY=VALUE:OTHER=VALUE` format. Escape literal colons as `\:` or quote the complete value.
243
+ - `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.
244
+ - `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.
245
+ - `CONTAINER_APT_PACKAGES`: Space-delimited system packages installed in the generated image
246
+ - `CONTAINER_CUSTOM_ENTRYPOINT`: Custom container entrypoint script. The value must match a key in `[project].scripts`.
247
+ - `CONTAINER_DEPS_IMAGE`: Existing dependency image used when neither `CONTAINER_DEPS_CONTENT` nor `CONTAINER_DEPS_FILE` is configured
244
248
  - `CONTAINER_EXTENSION_FILES`: Colon-delimited local extension Dockerfile paths. Escape literal colons as `\:` or quote the complete path.
245
249
  - `CONTAINER_EXTENSIONS`: Colon-delimited extension-bundle names or parameterized values. Escape literal colons as `\:` or quote the complete value.
246
250
  - `CONTAINER_ENV`: Colon-delimited `KEY=VALUE` declarations injected into the rendered Dockerfile's builder stage. Escape literal colons as `\:` or quote the complete value.
247
251
  - `.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.
248
252
  - `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.
249
- - `CUSTOM_ENTRYPOINT`: Custom container entrypoint script. The value must match a key in `[project].scripts`.
250
253
  - `CONTAINER_DEPS_CONTENT`: Inline Dockerfile instructions for a dependency image that installs artifacts into `/tmp/deps`
251
254
  - `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.
252
255
  - `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.
@@ -293,6 +296,28 @@ addopts = "-ra"
293
296
 
294
297
  Docker Compose development-stack tasks are available when the `containers` tag is selected. The current stack supports FastAPI applications and an optional PostgreSQL database.
295
298
 
299
+ ### Native Docker build arguments
300
+
301
+ 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.
302
+
303
+ ```shell
304
+ poe build-image --single-arch -- \
305
+ --secret id=pip_conf,env=PIP_CONF \
306
+ --ssh default \
307
+ --add-host example:127.0.0.1
308
+ ```
309
+
310
+ 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.
311
+
312
+ Settings that affect generated Dockerfile content or dependency-image orchestration can be persisted directly in the project configuration.
313
+
314
+ ```toml
315
+ [tool.poe.env]
316
+ CONTAINER_APT_PACKAGES = "curl jq"
317
+ CONTAINER_CUSTOM_ENTRYPOINT = "serve"
318
+ CONTAINER_DEPS_IMAGE = "example/dependencies:latest"
319
+ ```
320
+
296
321
  ### Stack configuration
297
322
 
298
323
  Set `COMPOSE_TYPE` to select the application stack. `fastapi` is currently supported and includes optional database support and Alembic migrations.
@@ -388,10 +413,11 @@ See the [standard Dockerfile template](src/common_python_tasks/data/generic/Dock
388
413
 
389
414
  - Multi-stage build: The build stage installs the selected package-manager backend and builds a wheel. The runtime stage installs only the wheel to keep the final image slim and reproducible.
390
415
  - Cache mounts: Pip and package-manager cache mounts speed up iterative builds without bloating the final image.
391
- - Explicit build arguments: `PYTHON_VERSION`, `PACKAGE_MANAGER`, `PACKAGE_MANAGER_VERSION`, `PACKAGE_NAME`, `AUTHORS`, `GIT_COMMIT`, and `CUSTOM_ENTRYPOINT` make image metadata and behavior predictable and auditable.
416
+ - Explicit build metadata: `PYTHON_VERSION`, `PACKAGE_MANAGER`, `PACKAGE_MANAGER_VERSION`, `PACKAGE_NAME`, `AUTHORS`, and `GIT_COMMIT` make image metadata predictable and auditable.
417
+ - 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.
392
418
  - 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.
393
419
  - Stable package path: Symlinks give entrypoints and consumers consistent `/pkg` and `/_$PACKAGE_NAME` paths regardless of wheel layout.
394
- - Safe entrypoint selection: The default entrypoint resolves the console script matching the package name and falls back to `python`. `CUSTOM_ENTRYPOINT` is validated against `[project].scripts`.
420
+ - 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`.
395
421
  - Minimal final image: The standard slim Python base, cache cleanup, and explicit `runtime` final target keep the default image small.
396
422
 
397
423
  ## Project notes
@@ -11,9 +11,9 @@ It supplies sensible defaults for formatting, linting, testing, packaging, relea
11
11
  Add `common-python-tasks` as a development dependency from your project root.
12
12
 
13
13
  ```shell
14
- uv add --dev common-python-tasks==0.7.0
14
+ uv add --dev common-python-tasks==0.8.0
15
15
  # or, for Poetry projects
16
- poetry add --group dev common-python-tasks==0.7.0
16
+ poetry add --group dev common-python-tasks==0.8.0
17
17
  ```
18
18
 
19
19
  Configure Poe the Poet to expose the default `common` task set.
@@ -38,7 +38,7 @@ The helper script performs the same development-dependency installation and Poe
38
38
  ```shell
39
39
  curl --fail --silent --show-error --location \
40
40
  --output /tmp/add_common_python_tasks.py \
41
- https://raw.githubusercontent.com/ci-sourcerer/common-python-tasks/v0.7.0/scripts/add_common_python_tasks.py
41
+ https://raw.githubusercontent.com/ci-sourcerer/common-python-tasks/v0.8.0/scripts/add_common_python_tasks.py
42
42
  ```
43
43
 
44
44
  Review the downloaded script before executing it.
@@ -50,10 +50,10 @@ less /tmp/add_common_python_tasks.py
50
50
  Run the reviewed script with the same pinned package version.
51
51
 
52
52
  ```shell
53
- COMMON_PYTHON_TASKS_VERSION=0.7.0 python3 /tmp/add_common_python_tasks.py
53
+ COMMON_PYTHON_TASKS_VERSION=0.8.0 python3 /tmp/add_common_python_tasks.py
54
54
  ```
55
55
 
56
- To install another release, replace both occurrences of `0.7.0` with that release's version.
56
+ To install another release, replace both occurrences of `0.8.0` with that release's version.
57
57
 
58
58
  ## Available tasks
59
59
 
@@ -164,7 +164,7 @@ After installing the package, a minimal project configuration looks like this.
164
164
  ```toml
165
165
  [project]
166
166
  name = "simple-cli-tool"
167
- version = "0.7.0"
167
+ version = "0.8.0"
168
168
 
169
169
  [tool.poe]
170
170
  include_script = "common_python_tasks:tasks()"
@@ -208,13 +208,16 @@ addopts = "-ra"
208
208
 
209
209
  - `CONTAINER_REGISTRY_USERNAME`: Container-registry username for image tagging; the default is the current local user
210
210
  - `CONTAINER_REGISTRY_URL`: Registry URL with a default of `docker.io/{username}`
211
- - `CONTAINER_BUILD_ARGS`: Additional Docker build arguments in `KEY=VALUE:OTHER=VALUE` format. Escape literal colons as `\:` or quote the complete value.
211
+ - `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.
212
+ - `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.
213
+ - `CONTAINER_APT_PACKAGES`: Space-delimited system packages installed in the generated image
214
+ - `CONTAINER_CUSTOM_ENTRYPOINT`: Custom container entrypoint script. The value must match a key in `[project].scripts`.
215
+ - `CONTAINER_DEPS_IMAGE`: Existing dependency image used when neither `CONTAINER_DEPS_CONTENT` nor `CONTAINER_DEPS_FILE` is configured
212
216
  - `CONTAINER_EXTENSION_FILES`: Colon-delimited local extension Dockerfile paths. Escape literal colons as `\:` or quote the complete path.
213
217
  - `CONTAINER_EXTENSIONS`: Colon-delimited extension-bundle names or parameterized values. Escape literal colons as `\:` or quote the complete value.
214
218
  - `CONTAINER_ENV`: Colon-delimited `KEY=VALUE` declarations injected into the rendered Dockerfile's builder stage. Escape literal colons as `\:` or quote the complete value.
215
219
  - `.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.
216
220
  - `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.
217
- - `CUSTOM_ENTRYPOINT`: Custom container entrypoint script. The value must match a key in `[project].scripts`.
218
221
  - `CONTAINER_DEPS_CONTENT`: Inline Dockerfile instructions for a dependency image that installs artifacts into `/tmp/deps`
219
222
  - `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.
220
223
  - `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.
@@ -261,6 +264,28 @@ addopts = "-ra"
261
264
 
262
265
  Docker Compose development-stack tasks are available when the `containers` tag is selected. The current stack supports FastAPI applications and an optional PostgreSQL database.
263
266
 
267
+ ### Native Docker build arguments
268
+
269
+ 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.
270
+
271
+ ```shell
272
+ poe build-image --single-arch -- \
273
+ --secret id=pip_conf,env=PIP_CONF \
274
+ --ssh default \
275
+ --add-host example:127.0.0.1
276
+ ```
277
+
278
+ 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.
279
+
280
+ Settings that affect generated Dockerfile content or dependency-image orchestration can be persisted directly in the project configuration.
281
+
282
+ ```toml
283
+ [tool.poe.env]
284
+ CONTAINER_APT_PACKAGES = "curl jq"
285
+ CONTAINER_CUSTOM_ENTRYPOINT = "serve"
286
+ CONTAINER_DEPS_IMAGE = "example/dependencies:latest"
287
+ ```
288
+
264
289
  ### Stack configuration
265
290
 
266
291
  Set `COMPOSE_TYPE` to select the application stack. `fastapi` is currently supported and includes optional database support and Alembic migrations.
@@ -356,10 +381,11 @@ See the [standard Dockerfile template](src/common_python_tasks/data/generic/Dock
356
381
 
357
382
  - Multi-stage build: The build stage installs the selected package-manager backend and builds a wheel. The runtime stage installs only the wheel to keep the final image slim and reproducible.
358
383
  - Cache mounts: Pip and package-manager cache mounts speed up iterative builds without bloating the final image.
359
- - Explicit build arguments: `PYTHON_VERSION`, `PACKAGE_MANAGER`, `PACKAGE_MANAGER_VERSION`, `PACKAGE_NAME`, `AUTHORS`, `GIT_COMMIT`, and `CUSTOM_ENTRYPOINT` make image metadata and behavior predictable and auditable.
384
+ - Explicit build metadata: `PYTHON_VERSION`, `PACKAGE_MANAGER`, `PACKAGE_MANAGER_VERSION`, `PACKAGE_NAME`, `AUTHORS`, and `GIT_COMMIT` make image metadata predictable and auditable.
385
+ - 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.
360
386
  - 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.
361
387
  - Stable package path: Symlinks give entrypoints and consumers consistent `/pkg` and `/_$PACKAGE_NAME` paths regardless of wheel layout.
362
- - Safe entrypoint selection: The default entrypoint resolves the console script matching the package name and falls back to `python`. `CUSTOM_ENTRYPOINT` is validated against `[project].scripts`.
388
+ - 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`.
363
389
  - Minimal final image: The standard slim Python base, cache cleanup, and explicit `runtime` final target keep the default image small.
364
390
 
365
391
  ## Project notes
@@ -37,12 +37,16 @@ Source = "http://github.com/ci-sourcerer/common-python-tasks.git"
37
37
  Issues = "http://github.com/ci-sourcerer/common-python-tasks/issues"
38
38
 
39
39
  [tool.poe]
40
- include_script = "common_python_tasks:tasks(exclude_tags=['containers'])"
40
+ include_script = "common_python_tasks:tasks(exclude_tags=['containers', 'fastapi'])"
41
41
 
42
42
  [tool.poe.env]
43
43
  PYTHONPATH = "src"
44
44
  RELEASE_UPDATE_CHANGELOG = "1"
45
- RELEASE_PRE_SCRIPT = "RELEASE_SCRIPT_PHASE=pre python scripts/release_script.py"
45
+ RELEASE_PRE_SCRIPT = "RELEASE_SCRIPT_PHASE=pre python -m scripts.release_script"
46
+ CONTAINER_APT_PACKAGES = "jq"
47
+
48
+ [tool.poe.tasks]
49
+ update-readme-tasks-table = { cmd = "python -m scripts.update_readme_tasks_table" }
46
50
 
47
51
  [tool.hatch.version]
48
52
  source = "uv-dynamic-versioning"
@@ -0,0 +1,78 @@
1
+ import re
2
+
3
+ from common_python_tasks.__main__ import (
4
+ _get_task_docstring,
5
+ get_available_tasks,
6
+ get_task_tags,
7
+ )
8
+
9
+ TASKS_TABLE_PATTERN = r"(?ms)<!-- tasks-table -->.*?<!-- end-tasks-table -->"
10
+ TASK_CATEGORY_ORDER = (
11
+ "Daily development",
12
+ "Packaging and releases",
13
+ "Container images",
14
+ "Development stacks",
15
+ )
16
+
17
+
18
+ def _get_task_category(task_name: str) -> str:
19
+ tags = get_task_tags(task_name) or []
20
+ if "web" in tags or "database" in tags:
21
+ return "Development stacks"
22
+ if task_name == "release":
23
+ return "Packaging and releases"
24
+ if "containers" in tags:
25
+ return "Container images"
26
+ if "packaging" in tags or "release" in tags:
27
+ return "Packaging and releases"
28
+ return "Daily development"
29
+
30
+
31
+ def _get_task_description(task_name: str) -> str:
32
+ return " ".join(
33
+ line.strip()
34
+ for line in (_get_task_docstring(task_name) or "").splitlines()
35
+ if line.strip()
36
+ )
37
+
38
+
39
+ def build_tasks_table() -> str:
40
+ """Build the markdown task table for README insertion."""
41
+ tasks_by_category = {category: [] for category in TASK_CATEGORY_ORDER}
42
+ for task_name in get_available_tasks(internal=False):
43
+ tasks_by_category[_get_task_category(task_name)].append(task_name)
44
+
45
+ lines = ["<!-- tasks-table -->"]
46
+ for category in TASK_CATEGORY_ORDER:
47
+ if not tasks_by_category[category]:
48
+ continue
49
+
50
+ lines.extend(
51
+ [
52
+ "",
53
+ f"### {category}",
54
+ "",
55
+ "| Task | Description | Tags |",
56
+ "| --- | --- | --- |",
57
+ ]
58
+ )
59
+ lines.extend(
60
+ f"| `{task_name}` | {_get_task_description(task_name)} | "
61
+ f"{', '.join(get_task_tags(task_name) or [])} |"
62
+ for task_name in tasks_by_category[category]
63
+ )
64
+
65
+ lines.extend(["", "<!-- end-tasks-table -->"])
66
+ return "\n".join(lines)
67
+
68
+
69
+ def replace_tasks_table(readme_text: str) -> str:
70
+ """Replace the task table in the README text with the generated task table.
71
+
72
+ Args:
73
+ readme_text: The text of the README.md file to update.
74
+
75
+ Returns:
76
+ The updated README.md text with the task table replaced.
77
+ """
78
+ return re.sub(TASKS_TABLE_PATTERN, build_tasks_table(), readme_text)
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ from enum import StrEnum
7
+ from pathlib import Path
8
+
9
+ from .readme_tasks_table import replace_tasks_table
10
+ from .utils import commit_readme_update, configure_logger, get_logger, log_dry_run
11
+
12
+ LOGGER = get_logger(__name__)
13
+
14
+
15
+ class ReleasePhase(StrEnum):
16
+ PRE = "pre"
17
+
18
+
19
+ RELEASE_SCRIPT_PHASE = "RELEASE_SCRIPT_PHASE"
20
+
21
+
22
+ def _get_latest_release_version() -> str:
23
+ completed_process = subprocess.run(
24
+ ["git", "tag", "--sort=-version:refname"],
25
+ capture_output=True,
26
+ check=True,
27
+ text=True,
28
+ )
29
+ latest_tag = next(
30
+ (
31
+ line.strip()
32
+ for line in completed_process.stdout.splitlines()
33
+ if line.strip()
34
+ ),
35
+ None,
36
+ )
37
+ if latest_tag is None:
38
+ sys.exit("Unable to determine the latest Git tag for README updates")
39
+
40
+ return latest_tag.removeprefix("v")
41
+
42
+
43
+ def _resolve_release_phase(phase: ReleasePhase | None = None) -> ReleasePhase:
44
+ if phase is not None:
45
+ return phase
46
+
47
+ phase_value = os.environ.get(RELEASE_SCRIPT_PHASE)
48
+ if phase_value is None:
49
+ sys.exit(f"{RELEASE_SCRIPT_PHASE} must be set to 'pre'.")
50
+
51
+ if phase_value.lower() != "pre":
52
+ sys.exit(f"Invalid {RELEASE_SCRIPT_PHASE!r}: {phase_value!r}. Expected 'pre'.")
53
+ return ReleasePhase.PRE
54
+
55
+
56
+ def _update_readme_for_pre_release(
57
+ readme_text: str, current_version: str, release_version: str
58
+ ) -> str:
59
+ if current_version not in readme_text:
60
+ sys.exit(
61
+ "README.md does not contain the latest tagged version "
62
+ f"{current_version!r} to update"
63
+ )
64
+
65
+ return replace_tasks_table(readme_text.replace(current_version, release_version))
66
+
67
+
68
+ def main() -> None:
69
+ """Update README release content during release hook execution."""
70
+ _resolve_release_phase()
71
+ release_version = os.environ["RELEASE_VERSION"]
72
+
73
+ configure_logger(LOGGER)
74
+
75
+ if os.environ.get("RELEASE_SCRIPT_DRY_RUN") == "1":
76
+ current_version = _get_latest_release_version()
77
+ log_dry_run(
78
+ LOGGER,
79
+ "Would modify: README.md (replace %r with %r)",
80
+ current_version,
81
+ release_version,
82
+ )
83
+ return
84
+
85
+ readme_path = Path("README.md")
86
+ readme_text = readme_path.read_text(encoding="utf-8")
87
+
88
+ updated_text = _update_readme_for_pre_release(
89
+ readme_text,
90
+ _get_latest_release_version(),
91
+ release_version,
92
+ )
93
+
94
+ if updated_text == readme_text:
95
+ sys.exit("README.md was not changed")
96
+
97
+ readme_path.write_text(updated_text, encoding="utf-8")
98
+ commit_readme_update(f"chore(release): set README version {release_version}")
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .readme_tasks_table import replace_tasks_table
8
+ from .utils import commit_readme_update, configure_logger, get_logger, log_dry_run
9
+
10
+ LOGGER = get_logger(__name__)
11
+
12
+
13
+ def main() -> None:
14
+ """Update the README task table from the current task definitions."""
15
+ configure_logger(LOGGER)
16
+
17
+ readme_path = Path("README.md")
18
+ readme_text = readme_path.read_text(encoding="utf-8")
19
+
20
+ if os.environ.get("RELEASE_SCRIPT_DRY_RUN") == "1":
21
+ updated_text = replace_tasks_table(readme_text)
22
+ if updated_text == readme_text:
23
+ log_dry_run(LOGGER, "README.md task table is already up to date")
24
+ else:
25
+ log_dry_run(LOGGER, "Would update README.md task table")
26
+ return
27
+
28
+ updated_text = replace_tasks_table(readme_text)
29
+ if updated_text == readme_text:
30
+ sys.exit("README.md task table was not changed")
31
+
32
+ readme_path.write_text(updated_text, encoding="utf-8")
33
+ commit_readme_update("chore(docs): refresh README task table")
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,76 @@
1
+ import logging
2
+ import subprocess
3
+
4
+
5
+ class ColoredFormatter(logging.Formatter):
6
+ """Format warning and error log levels with ANSI color codes."""
7
+
8
+ COLORS = {
9
+ "WARNING": "\033[93m",
10
+ "ERROR": "\033[91m",
11
+ "CRITICAL": "\033[91m",
12
+ "RESET": "\033[0m",
13
+ }
14
+
15
+ def format(self, record: logging.LogRecord) -> str:
16
+ """Format a log record with a colored level name.
17
+
18
+ Args:
19
+ record: The log record to format.
20
+
21
+ Returns:
22
+ The formatted log message.
23
+ """
24
+ log_color = self.COLORS.get(record.levelname, "")
25
+ record.levelname = f"\033[1m{log_color}{record.levelname}{self.COLORS['RESET'] if log_color else ''}\033[0m"
26
+ return super().format(record)
27
+
28
+
29
+ def configure_logger(logger: logging.Logger) -> None:
30
+ """Configure a logger with the standard colored console formatter.
31
+
32
+ Args:
33
+ logger: Logger to configure.
34
+ """
35
+ while logger.handlers:
36
+ logger.removeHandler(logger.handlers[0])
37
+
38
+ handler = logging.StreamHandler()
39
+ handler.setFormatter(ColoredFormatter("[%(asctime)s] %(levelname)s: %(message)s"))
40
+ logger.addHandler(handler)
41
+
42
+
43
+ def get_logger(name: str) -> logging.Logger:
44
+ """Create a standard logger for a repository maintenance script.
45
+
46
+ Args:
47
+ name: Logger name, usually the script's `__name__` value.
48
+
49
+ Returns:
50
+ A configured logger without parent propagation.
51
+ """
52
+ logger = logging.getLogger(name)
53
+ logger.propagate = False
54
+ logger.setLevel(logging.INFO)
55
+ return logger
56
+
57
+
58
+ def log_dry_run(logger: logging.Logger, message: str, *args: object) -> None:
59
+ """Log a dry-run informational message with a yellow prefix.
60
+
61
+ Args:
62
+ logger: Logger used to emit the message.
63
+ message: Log message template.
64
+ *args: Values interpolated into `message`.
65
+ """
66
+ logger.info("\033[93m[DRY RUN]\033[0m " + message, *args)
67
+
68
+
69
+ def commit_readme_update(commit_message: str) -> None:
70
+ """Commit the current README changes.
71
+
72
+ Args:
73
+ commit_message: Message for the README update commit.
74
+ """
75
+ subprocess.run(["git", "add", "README.md"], check=True)
76
+ subprocess.run(["git", "commit", "-m", commit_message], check=True)
@@ -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:
@@ -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"]