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.
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/.gitignore +2 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/PKG-INFO +38 -12
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/README.md +36 -10
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/pyproject.toml +6 -2
- common_python_tasks-0.8.0/scripts/readme_tasks_table.py +78 -0
- common_python_tasks-0.8.0/scripts/release_script.py +102 -0
- common_python_tasks-0.8.0/scripts/update_readme_tasks_table.py +37 -0
- common_python_tasks-0.8.0/scripts/utils.py +76 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/__init__.py +9 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/__main__.py +19 -3
- common_python_tasks-0.8.0/src/common_python_tasks/data/dockerfile_extensions/.gitkeep +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/Dockerfile.j2 +5 -5
- common_python_tasks-0.8.0/src/common_python_tasks/data/generic/ruff.toml +2 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/docker.py +294 -154
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/docker_compose.py +12 -4
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/env.py +82 -36
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/git.py +30 -16
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/github.py +17 -2
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/project.py +2 -2
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/tasks.py +138 -157
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/utils.py +29 -4
- common_python_tasks-0.7.0/scripts/release_script.py +0 -214
- common_python_tasks-0.7.0/src/common_python_tasks/data/generic/ruff.toml +0 -6
- {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
- {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
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/LICENSE +0 -0
- /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
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/scripts/add_common_python_tasks.py +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/alembic.ini.j2 +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/compose-base.yml.j2 +0 -0
- {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
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/compose-db.yml.j2 +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/fastapi/compose-debug.yml.j2 +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/.coveragerc +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/.dockerignore +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/Dockerfile.deps.j2 +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/cliff.toml +0 -0
- {common_python_tasks-0.7.0 → common_python_tasks-0.8.0}/src/common_python_tasks/data/generic/pytest.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: common-python-tasks
|
|
3
|
-
Version: 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
- `
|
|
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
|
|
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`. `
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
- `
|
|
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
|
|
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`. `
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
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:
|
|
File without changes
|
|
@@ -93,17 +93,17 @@ ENV {{ env_var }}
|
|
|
93
93
|
{%- endif %}
|
|
94
94
|
|
|
95
95
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
96
|
-
{% if
|
|
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 {{
|
|
100
|
+
apt-get update && apt-get install -y --no-install-recommends {{ CONTAINER_APT_PACKAGES }}
|
|
101
101
|
{% endif %}
|
|
102
|
-
{% if
|
|
102
|
+
{% if CONTAINER_DEPS_IMAGE %}
|
|
103
103
|
|
|
104
104
|
# Pull external dependencies from the pre-built deps image
|
|
105
|
-
ARG
|
|
106
|
-
COPY --from={{
|
|
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; \
|