common-python-tasks 0.3.1__py3-none-any.whl → 0.4.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.
- common_python_tasks/data/fastapi/compose-base.yml.j2 +2 -1
- common_python_tasks/data/fastapi/compose-db.yml.j2 +2 -1
- common_python_tasks/data/generic/.dockerignore +0 -2
- common_python_tasks/data/generic/Dockerfile.j2 +45 -8
- common_python_tasks/docker.py +128 -57
- common_python_tasks/docker_compose.py +15 -9
- common_python_tasks/env.py +53 -12
- common_python_tasks/git.py +27 -6
- common_python_tasks/github.py +19 -40
- common_python_tasks/project.py +316 -26
- common_python_tasks/tasks.py +185 -92
- common_python_tasks/utils.py +190 -13
- common_python_tasks-0.4.1.dist-info/METADATA +436 -0
- common_python_tasks-0.4.1.dist-info/RECORD +28 -0
- {common_python_tasks-0.3.1.dist-info → common_python_tasks-0.4.1.dist-info}/WHEEL +1 -1
- common_python_tasks-0.3.1.dist-info/METADATA +0 -428
- common_python_tasks-0.3.1.dist-info/RECORD +0 -28
- {common_python_tasks-0.3.1.dist-info → common_python_tasks-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,7 +9,8 @@ x-common-environment: &common-environment
|
|
|
9
9
|
x-common-build-args: &common-build-args
|
|
10
10
|
PACKAGE_NAME: ${PACKAGE_NAME}
|
|
11
11
|
PYTHON_VERSION: ${PYTHON_VERSION}
|
|
12
|
-
|
|
12
|
+
PACKAGE_MANAGER: ${PACKAGE_MANAGER}
|
|
13
|
+
PACKAGE_MANAGER_VERSION: ${PACKAGE_MANAGER_VERSION}
|
|
13
14
|
|
|
14
15
|
services:
|
|
15
16
|
api:
|
|
@@ -8,7 +8,8 @@ x-common-environment: &common-environment
|
|
|
8
8
|
x-common-build-args: &common-build-args
|
|
9
9
|
PACKAGE_NAME: ${PACKAGE_NAME}
|
|
10
10
|
PYTHON_VERSION: ${PYTHON_VERSION}
|
|
11
|
-
|
|
11
|
+
PACKAGE_MANAGER: ${PACKAGE_MANAGER}
|
|
12
|
+
PACKAGE_MANAGER_VERSION: ${PACKAGE_MANAGER_VERSION}
|
|
12
13
|
WORKDIR_PATH: ${WORKDIR_PATH}
|
|
13
14
|
|
|
14
15
|
services:
|
|
@@ -7,20 +7,48 @@ ARG PYTHON_VERSION=3
|
|
|
7
7
|
# See https://hub.docker.com/_/python for available variants
|
|
8
8
|
ARG PYTHON_VARIANT=slim
|
|
9
9
|
ARG WORKDIR_PATH=/workspace
|
|
10
|
+
{% set package_manager = PACKAGE_MANAGER|default('poetry') %}
|
|
10
11
|
|
|
11
12
|
FROM python:${PYTHON_VERSION} AS builder
|
|
12
13
|
|
|
14
|
+
ARG PACKAGE_VERSION
|
|
15
|
+
|
|
16
|
+
{% if package_manager == 'poetry' %}
|
|
13
17
|
ARG POETRY_VERSION
|
|
14
18
|
ARG POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION
|
|
15
19
|
ARG POETRY_PLUGIN_EXPORT_VERSION
|
|
16
|
-
ARG TOMLKIT_VERSION
|
|
17
20
|
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
# Bypass VCS-based version detection: use the host-computed version directly.
|
|
22
|
+
# This avoids requiring a .git directory in the build context and prevents
|
|
23
|
+
# false dirty-version detection from a partial working tree.
|
|
24
|
+
ENV POETRY_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION}
|
|
25
|
+
|
|
26
|
+
# Install Poetry and required Poetry plugins for package build
|
|
27
|
+
RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }} \
|
|
28
|
+
sh -c 'set -eu; \
|
|
29
|
+
poetry_spec="poetry"; \
|
|
30
|
+
if [ -n "${POETRY_VERSION:-}" ]; then poetry_spec="poetry==$POETRY_VERSION"; fi; \
|
|
31
|
+
dynamic_spec="poetry-dynamic-versioning[plugin]"; \
|
|
32
|
+
if [ -n "${POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION:-}" ]; then dynamic_spec="poetry-dynamic-versioning[plugin]==$POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION"; fi; \
|
|
33
|
+
export_spec="poetry-plugin-export"; \
|
|
34
|
+
if [ -n "${POETRY_PLUGIN_EXPORT_VERSION:-}" ]; then export_spec="poetry-plugin-export==$POETRY_PLUGIN_EXPORT_VERSION"; fi; \
|
|
35
|
+
pip install --root-user-action=ignore "$poetry_spec" "$dynamic_spec" "$export_spec"'
|
|
36
|
+
{% else %}
|
|
37
|
+
ARG UV_VERSION
|
|
38
|
+
|
|
39
|
+
# Bypass VCS-based version detection: use the host-computed version directly.
|
|
40
|
+
# This avoids requiring a .git directory in the build context and prevents
|
|
41
|
+
# false dirty-version detection from a partial working tree.
|
|
42
|
+
ENV UV_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION}
|
|
43
|
+
|
|
44
|
+
# Install uv for package build and dependency export steps
|
|
45
|
+
RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }} \
|
|
46
|
+
sh -c 'if [ -n "${UV_VERSION:-}" ]; then \
|
|
47
|
+
pip install --root-user-action=ignore "uv==$UV_VERSION"; \
|
|
48
|
+
else \
|
|
49
|
+
pip install --root-user-action=ignore uv; \
|
|
50
|
+
fi'
|
|
51
|
+
{% endif %}
|
|
24
52
|
|
|
25
53
|
# Build package
|
|
26
54
|
WORKDIR /tmp/build
|
|
@@ -28,15 +56,24 @@ COPY . /tmp/build/
|
|
|
28
56
|
|
|
29
57
|
{% if HAS_DEBUG_DEPS %}
|
|
30
58
|
# Export debug requirements when the project defines a debug dependency group
|
|
59
|
+
{% if package_manager == 'poetry' %}
|
|
31
60
|
RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }} \
|
|
32
61
|
poetry export --only debug --without-hashes -f requirements.txt --output requirements-debug.txt
|
|
62
|
+
{% else %}
|
|
63
|
+
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }} \
|
|
64
|
+
uv export --group debug --no-hashes --format requirements-txt --output-file requirements-debug.txt
|
|
65
|
+
{% endif %}
|
|
33
66
|
{% endif %}
|
|
34
67
|
|
|
35
|
-
# Build the wheel, caching
|
|
68
|
+
# Build the wheel, caching the package-manager cache directory to speed up subsequent builds.
|
|
36
69
|
# If running this in a CI system, you must be using a persistent builder to take
|
|
37
70
|
# advantage of the cache mount, and you should configure your CI to persist the
|
|
38
71
|
# cache directory between builds
|
|
72
|
+
{% if package_manager == 'poetry' %}
|
|
39
73
|
RUN --mount=type=cache,target=/root/.cache/pypoetry,id=pypoetry-cache{{ CACHE_ID_SUFFIX }} poetry build --format=wheel
|
|
74
|
+
{% else %}
|
|
75
|
+
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }} uv build --wheel
|
|
76
|
+
{% endif %}
|
|
40
77
|
|
|
41
78
|
FROM python:${PYTHON_VERSION}-${PYTHON_VARIANT} AS runtime
|
|
42
79
|
ARG WORKDIR_PATH=/workspace
|
common_python_tasks/docker.py
CHANGED
|
@@ -93,7 +93,6 @@ def _safe_unlink(path: Path | str | None) -> None:
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def _build_docker_build_command(
|
|
96
|
-
*,
|
|
97
96
|
context_path: Path,
|
|
98
97
|
dockerfile_path: Path,
|
|
99
98
|
build_args: dict[str, str],
|
|
@@ -152,35 +151,60 @@ def prune_images_keep(
|
|
|
152
151
|
|
|
153
152
|
retain_count = int(keep) + 1
|
|
154
153
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
154
|
+
def _list_images_for_reference(reference: str) -> list[str]:
|
|
155
|
+
res = utils.run_command(
|
|
156
|
+
[
|
|
157
|
+
"docker",
|
|
158
|
+
"image",
|
|
159
|
+
"ls",
|
|
160
|
+
"--format",
|
|
161
|
+
"{{.Repository}}:{{.Tag}}",
|
|
162
|
+
"--filter",
|
|
163
|
+
f"reference={reference}:*",
|
|
164
|
+
],
|
|
165
|
+
capture_output=True,
|
|
166
|
+
acceptable_returncodes={0, 1},
|
|
167
|
+
)
|
|
168
|
+
if res.returncode != 0:
|
|
169
|
+
LOGGER.warning("Failed to list images for pruning: %s", reference)
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
return [
|
|
173
|
+
line.strip()
|
|
174
|
+
for line in res.stdout.splitlines()
|
|
175
|
+
if line.strip() and not line.strip().startswith("<none>")
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
parsed_full_name = utils.parse_image_reference(full_name)
|
|
179
|
+
short_reference = utils.compose_image_name(package_name, fully_qualified=False)
|
|
180
|
+
full_reference = utils.compose_image_name(
|
|
181
|
+
package_name,
|
|
182
|
+
registry=parsed_full_name["registry"],
|
|
183
|
+
namespace=parsed_full_name["namespace"],
|
|
167
184
|
)
|
|
168
|
-
if res.returncode != 0:
|
|
169
|
-
LOGGER.exception("Failed to list images for pruning: %s", full_name)
|
|
170
|
-
return
|
|
171
185
|
|
|
172
|
-
lines =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
186
|
+
lines = _list_images_for_reference(full_reference)
|
|
187
|
+
lines += [
|
|
188
|
+
line
|
|
189
|
+
for line in _list_images_for_reference(short_reference)
|
|
190
|
+
if line not in lines
|
|
176
191
|
]
|
|
177
|
-
|
|
192
|
+
|
|
193
|
+
tags_in_order: list[str] = []
|
|
194
|
+
seen_tags: set[str] = set()
|
|
178
195
|
for entry in lines:
|
|
179
|
-
|
|
196
|
+
try:
|
|
197
|
+
parsed_entry = utils.parse_image_reference(entry)
|
|
198
|
+
except ValueError:
|
|
180
199
|
continue
|
|
181
|
-
|
|
182
|
-
if
|
|
183
|
-
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
parsed_entry["repository"] == package_name
|
|
203
|
+
and parsed_entry["tag"]
|
|
204
|
+
and parsed_entry["tag"] not in seen_tags
|
|
205
|
+
):
|
|
206
|
+
tags_in_order.append(parsed_entry["tag"])
|
|
207
|
+
seen_tags.add(parsed_entry["tag"])
|
|
184
208
|
|
|
185
209
|
candidates = [t for t in tags_in_order if t not in protect_tags]
|
|
186
210
|
if len(candidates) <= retain_count:
|
|
@@ -188,7 +212,18 @@ def prune_images_keep(
|
|
|
188
212
|
|
|
189
213
|
to_delete = candidates[retain_count:]
|
|
190
214
|
for tag in to_delete:
|
|
191
|
-
|
|
215
|
+
short_image = utils.compose_image_name(
|
|
216
|
+
package_name,
|
|
217
|
+
tag=tag,
|
|
218
|
+
fully_qualified=False,
|
|
219
|
+
)
|
|
220
|
+
full_image = utils.compose_image_name(
|
|
221
|
+
package_name,
|
|
222
|
+
tag=tag,
|
|
223
|
+
registry=parsed_full_name["registry"],
|
|
224
|
+
namespace=parsed_full_name["namespace"],
|
|
225
|
+
)
|
|
226
|
+
for img in (short_image, full_image):
|
|
192
227
|
try:
|
|
193
228
|
LOGGER.info("Pruning image %s", img)
|
|
194
229
|
utils.run_command(["docker", "rmi", img], acceptable_returncodes={0, 1})
|
|
@@ -295,12 +330,12 @@ def build_deps_image(
|
|
|
295
330
|
|
|
296
331
|
full_tag = f"{deps_image_name}:{deps_tag}"
|
|
297
332
|
build_cmd = _build_docker_build_command(
|
|
298
|
-
context_path
|
|
299
|
-
dockerfile_path
|
|
300
|
-
build_args
|
|
301
|
-
archs
|
|
302
|
-
no_cache
|
|
303
|
-
plain
|
|
333
|
+
context_path,
|
|
334
|
+
dockerfile_path,
|
|
335
|
+
build_args,
|
|
336
|
+
archs,
|
|
337
|
+
no_cache,
|
|
338
|
+
plain,
|
|
304
339
|
tags=[full_tag],
|
|
305
340
|
)
|
|
306
341
|
|
|
@@ -369,6 +404,7 @@ def build_image(
|
|
|
369
404
|
try:
|
|
370
405
|
archs = _resolve_archs(single_arch)
|
|
371
406
|
files_to_ignore = [Path(".dockerignore")] if temp_dockerignore_created else []
|
|
407
|
+
package_version = git.get_version(ignore=files_to_ignore)
|
|
372
408
|
version_string = git.get_image_tag(ignore=files_to_ignore)
|
|
373
409
|
|
|
374
410
|
dockerfile_source = (
|
|
@@ -400,26 +436,51 @@ def build_image(
|
|
|
400
436
|
tag = BuildTag.LATEST if not git.has_tags_later_in_history() else None
|
|
401
437
|
|
|
402
438
|
version_tag = f"{version_string}{suffix}"
|
|
403
|
-
|
|
439
|
+
if git.git_available():
|
|
440
|
+
commit_hash = utils.run_command(
|
|
441
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
442
|
+
capture_output=True,
|
|
443
|
+
acceptable_returncodes={0},
|
|
444
|
+
).stdout.strip()
|
|
445
|
+
dirty_suffix = (
|
|
446
|
+
"-dirty" if git.get_dirty_files(ignore=files_to_ignore) else ""
|
|
447
|
+
)
|
|
448
|
+
commit_tag = f"{commit_hash}{dirty_suffix}{suffix}"
|
|
449
|
+
else:
|
|
450
|
+
commit_tag = version_tag
|
|
451
|
+
|
|
404
452
|
python_version = platform.python_version()
|
|
405
|
-
|
|
453
|
+
package_manager = project.get_package_manager()
|
|
454
|
+
package_manager_version = project.get_package_manager_version(package_manager)
|
|
406
455
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
"POETRY_VERSION":
|
|
456
|
+
manager_build_args: dict[str, str | None]
|
|
457
|
+
if package_manager == project.PackageManager.POETRY:
|
|
458
|
+
manager_build_args = {
|
|
459
|
+
"POETRY_VERSION": package_manager_version,
|
|
411
460
|
"POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION": project.get_installed_requirement_version(
|
|
412
461
|
"poetry-dynamic-versioning[plugin]"
|
|
413
462
|
),
|
|
414
463
|
"POETRY_PLUGIN_EXPORT_VERSION": project.get_installed_requirement_version(
|
|
415
464
|
"poetry-plugin-export"
|
|
416
465
|
),
|
|
417
|
-
|
|
466
|
+
}
|
|
467
|
+
else:
|
|
468
|
+
manager_build_args = {
|
|
469
|
+
"UV_VERSION": package_manager_version,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
build_args = _merge_build_args(
|
|
473
|
+
{
|
|
474
|
+
"PYTHON_VERSION": python_version,
|
|
475
|
+
"PACKAGE_MANAGER": package_manager,
|
|
476
|
+
"PACKAGE_MANAGER_VERSION": package_manager_version,
|
|
477
|
+
"PACKAGE_VERSION": package_version,
|
|
418
478
|
"PACKAGE_NAME": utils.get_package_name(use_underscores=True),
|
|
419
479
|
"AUTHORS": ",".join(
|
|
420
480
|
[f"{name} <{email}>" for name, email in project.get_authors()]
|
|
421
481
|
),
|
|
422
482
|
"GIT_COMMIT": commit_tag,
|
|
483
|
+
**manager_build_args,
|
|
423
484
|
},
|
|
424
485
|
extra_build_args,
|
|
425
486
|
)
|
|
@@ -432,26 +493,36 @@ def build_image(
|
|
|
432
493
|
image_name if image_name is not None else utils.get_package_name()
|
|
433
494
|
)
|
|
434
495
|
orig_full_name = utils.get_full_image_name()
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
496
|
+
|
|
497
|
+
parsed_full_name = utils.parse_image_reference(orig_full_name)
|
|
498
|
+
short_tags = [
|
|
499
|
+
utils.compose_image_name(image_short_name, tag=t, fully_qualified=False)
|
|
500
|
+
for t in tags_to_use
|
|
501
|
+
]
|
|
502
|
+
full_tags = [
|
|
503
|
+
utils.compose_image_name(
|
|
504
|
+
image_short_name,
|
|
505
|
+
tag=t,
|
|
506
|
+
registry=parsed_full_name["registry"],
|
|
507
|
+
namespace=parsed_full_name["namespace"],
|
|
508
|
+
)
|
|
509
|
+
for t in tags_to_use
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
all_tags: list[str] = []
|
|
513
|
+
for resolved_tag in [*short_tags, *full_tags]:
|
|
514
|
+
if resolved_tag not in all_tags:
|
|
515
|
+
all_tags.append(resolved_tag)
|
|
516
|
+
|
|
443
517
|
build_cmd = _build_docker_build_command(
|
|
444
|
-
context_path
|
|
445
|
-
dockerfile_path
|
|
446
|
-
build_args
|
|
447
|
-
archs
|
|
448
|
-
no_cache
|
|
449
|
-
plain
|
|
518
|
+
context_path,
|
|
519
|
+
dockerfile_path,
|
|
520
|
+
build_args,
|
|
521
|
+
archs,
|
|
522
|
+
no_cache,
|
|
523
|
+
plain,
|
|
450
524
|
target=None if omit_target else target,
|
|
451
|
-
tags=
|
|
452
|
-
*[f"{image_short_name}:{t}" for t in tags_to_use],
|
|
453
|
-
*[f"{image_full_name}:{t}" for t in tags_to_use],
|
|
454
|
-
],
|
|
525
|
+
tags=all_tags,
|
|
455
526
|
)
|
|
456
527
|
utils.run_command(build_cmd)
|
|
457
528
|
delete_temp_file = True
|
|
@@ -14,7 +14,7 @@ from poethepoet_tasks import TaskCollection
|
|
|
14
14
|
|
|
15
15
|
from . import utils
|
|
16
16
|
from .env import get_workdir_path
|
|
17
|
-
from .project import
|
|
17
|
+
from .project import get_package_manager, get_package_manager_version
|
|
18
18
|
|
|
19
19
|
LOGGER = logging.getLogger(__name__)
|
|
20
20
|
|
|
@@ -232,7 +232,8 @@ _COMPOSE_VAR_REQUIREMENTS: dict[str, dict[str, set[str]]] = {
|
|
|
232
232
|
"ENVIRONMENT",
|
|
233
233
|
"IMAGE_TAG",
|
|
234
234
|
"PYTHON_VERSION",
|
|
235
|
-
"
|
|
235
|
+
"PACKAGE_MANAGER",
|
|
236
|
+
"PACKAGE_MANAGER_VERSION",
|
|
236
237
|
},
|
|
237
238
|
"compose-db": {
|
|
238
239
|
"PACKAGE_NAME",
|
|
@@ -242,7 +243,8 @@ _COMPOSE_VAR_REQUIREMENTS: dict[str, dict[str, set[str]]] = {
|
|
|
242
243
|
"DB_PORT",
|
|
243
244
|
"IMAGE_TAG",
|
|
244
245
|
"PYTHON_VERSION",
|
|
245
|
-
"
|
|
246
|
+
"PACKAGE_MANAGER",
|
|
247
|
+
"PACKAGE_MANAGER_VERSION",
|
|
246
248
|
"POSTGRES_VERSION",
|
|
247
249
|
"WORKDIR_PATH",
|
|
248
250
|
},
|
|
@@ -328,9 +330,10 @@ def get_compose_env(
|
|
|
328
330
|
"ENVIRONMENT": os.getenv("ENVIRONMENT", "production"),
|
|
329
331
|
"IMAGE_TAG": image_tag or "latest",
|
|
330
332
|
"PACKAGE_NAME": package_name,
|
|
333
|
+
"PACKAGE_MANAGER": get_package_manager(),
|
|
334
|
+
"PACKAGE_MANAGER_VERSION": get_package_manager_version(),
|
|
331
335
|
"PACKAGE_UNDERSCORE_NAME": utils.package_name_to_underscore(package_name),
|
|
332
336
|
"WORKDIR_PATH": get_workdir_path(),
|
|
333
|
-
"POETRY_VERSION": get_poetry_version(),
|
|
334
337
|
"POSTGRES_VERSION": os.getenv("POSTGRES_VERSION", "17"),
|
|
335
338
|
"PYTHON_VERSION": platform.python_version(),
|
|
336
339
|
"SECRET_KEY": os.getenv("SECRET_KEY", ""),
|
|
@@ -375,6 +378,8 @@ def load_compose_files(
|
|
|
375
378
|
cleaned up **after** `docker compose` down`.
|
|
376
379
|
"""
|
|
377
380
|
|
|
381
|
+
from .env import split_colon_delimited_values
|
|
382
|
+
|
|
378
383
|
compose_files_env = os.getenv("COMPOSE_FILE")
|
|
379
384
|
if compose_files_env:
|
|
380
385
|
LOGGER.debug(
|
|
@@ -382,14 +387,14 @@ def load_compose_files(
|
|
|
382
387
|
compose_files_env,
|
|
383
388
|
)
|
|
384
389
|
return (
|
|
385
|
-
[Path(
|
|
390
|
+
[Path(path) for path in split_colon_delimited_values(compose_files_env)],
|
|
386
391
|
[],
|
|
387
392
|
[],
|
|
388
393
|
)
|
|
389
394
|
|
|
390
|
-
compose_type =
|
|
395
|
+
compose_type = get_compose_type()
|
|
391
396
|
compose_addons_str = os.getenv("COMPOSE_ADDONS", "")
|
|
392
|
-
compose_addons =
|
|
397
|
+
compose_addons = split_colon_delimited_values(compose_addons_str)
|
|
393
398
|
|
|
394
399
|
LOGGER.debug(
|
|
395
400
|
"Loading compose files for type '%s' with addons: %s%s",
|
|
@@ -459,7 +464,7 @@ def load_compose_files(
|
|
|
459
464
|
|
|
460
465
|
overlay_files_str = os.getenv("COMPOSE_OVERLAY_FILES", "")
|
|
461
466
|
if overlay_files_str:
|
|
462
|
-
overlay_files =
|
|
467
|
+
overlay_files = split_colon_delimited_values(overlay_files_str)
|
|
463
468
|
LOGGER.debug("Adding overlay compose files: %s", overlay_files)
|
|
464
469
|
for overlay_file in overlay_files:
|
|
465
470
|
files_and_cleanups.append((Path(overlay_file), False))
|
|
@@ -497,6 +502,7 @@ def run_docker_compose_command(
|
|
|
497
502
|
*args: str | None,
|
|
498
503
|
compose_files: list[Path],
|
|
499
504
|
compose_env: dict[str, str],
|
|
505
|
+
tasks: TaskCollection,
|
|
500
506
|
) -> None:
|
|
501
507
|
"""Run `docker compose` with standard file and env-file arguments.
|
|
502
508
|
|
|
@@ -512,7 +518,7 @@ def run_docker_compose_command(
|
|
|
512
518
|
|
|
513
519
|
utils.run_command(
|
|
514
520
|
[
|
|
515
|
-
*compose_cmd_prefix(compose_files),
|
|
521
|
+
*compose_cmd_prefix(compose_files, tasks),
|
|
516
522
|
*[arg for arg in args if arg is not None],
|
|
517
523
|
],
|
|
518
524
|
env=compose_env,
|
common_python_tasks/env.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
import secrets
|
|
4
|
+
import shlex
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
from shlex import split as shlex_split
|
|
6
6
|
|
|
7
7
|
from . import utils
|
|
8
8
|
|
|
@@ -14,6 +14,7 @@ TRUTHY_VALUES = {
|
|
|
14
14
|
"yes",
|
|
15
15
|
"on",
|
|
16
16
|
"enabled",
|
|
17
|
+
"enable",
|
|
17
18
|
"y",
|
|
18
19
|
"t",
|
|
19
20
|
}
|
|
@@ -75,7 +76,7 @@ def parse_container_extensions() -> list[dict]:
|
|
|
75
76
|
exts: list[dict] = []
|
|
76
77
|
files_raw = os.getenv("CONTAINER_EXTENSION_FILES")
|
|
77
78
|
if files_raw:
|
|
78
|
-
for part in
|
|
79
|
+
for part in split_colon_delimited_values(files_raw):
|
|
79
80
|
pth = Path(part)
|
|
80
81
|
exts.append(
|
|
81
82
|
{
|
|
@@ -91,7 +92,7 @@ def parse_container_extensions() -> list[dict]:
|
|
|
91
92
|
)
|
|
92
93
|
bundles_raw = os.getenv("CONTAINER_EXTENSIONS")
|
|
93
94
|
if bundles_raw:
|
|
94
|
-
for part in (
|
|
95
|
+
for part in split_colon_delimited_values(bundles_raw):
|
|
95
96
|
if not part:
|
|
96
97
|
continue
|
|
97
98
|
if "=" in part:
|
|
@@ -180,24 +181,64 @@ def load_container_env_file(path: str | None = None) -> str | None:
|
|
|
180
181
|
return container_env if container_env else None
|
|
181
182
|
|
|
182
183
|
|
|
183
|
-
def
|
|
184
|
-
|
|
184
|
+
def split_delimited_values(
|
|
185
|
+
value: str,
|
|
186
|
+
separators: str = ":",
|
|
187
|
+
allow_whitespace: bool = False,
|
|
188
|
+
) -> list[str]:
|
|
189
|
+
"""Split a delimited string while preserving quoted substrings.
|
|
185
190
|
|
|
186
191
|
Args:
|
|
187
|
-
value: The
|
|
192
|
+
value: The string to split.
|
|
193
|
+
separators: Explicit separator characters to use.
|
|
194
|
+
allow_whitespace: Treat whitespace as additional separators.
|
|
188
195
|
|
|
189
196
|
Returns:
|
|
190
|
-
A list of strings representing the
|
|
197
|
+
A list of strings representing the parsed tokens.
|
|
191
198
|
"""
|
|
192
199
|
if not value or not value.strip():
|
|
193
200
|
return []
|
|
194
201
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
lexer = shlex.shlex(value, posix=True)
|
|
203
|
+
lexer.whitespace = separators + (" \t\r\n" if allow_whitespace else "")
|
|
204
|
+
lexer.whitespace_split = True
|
|
205
|
+
lexer.commenters = "#"
|
|
206
|
+
return [token for token in lexer if token.strip()]
|
|
199
207
|
|
|
200
|
-
|
|
208
|
+
|
|
209
|
+
def split_colon_delimited_values(value: str) -> list[str]:
|
|
210
|
+
"""Split a colon- or whitespace-delimited string while preserving quoted substrings."""
|
|
211
|
+
return split_delimited_values(value, separators=":", allow_whitespace=True)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def split_colon_or_whitespace_delimited_values(value: str) -> list[str]:
|
|
215
|
+
"""Split a colon- or whitespace-delimited string while preserving quoted substrings."""
|
|
216
|
+
return split_colon_delimited_values(value)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def parse_container_build_args(
|
|
220
|
+
cli_build_args: list[str] | None,
|
|
221
|
+
env_build_args: str | None,
|
|
222
|
+
) -> dict[str, str] | None:
|
|
223
|
+
"""Parse container build args from CLI or environment input."""
|
|
224
|
+
if cli_build_args is not None:
|
|
225
|
+
build_arg_tokens = [
|
|
226
|
+
token.strip() for token in cli_build_args if token and token.strip()
|
|
227
|
+
]
|
|
228
|
+
elif env_build_args:
|
|
229
|
+
build_arg_tokens = split_colon_delimited_values(env_build_args)
|
|
230
|
+
else:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
parsed_build_args: dict[str, str] = {}
|
|
234
|
+
for token in build_arg_tokens:
|
|
235
|
+
if "=" not in token:
|
|
236
|
+
LOGGER.warning("Ignoring invalid build-arg token: %s", token)
|
|
237
|
+
continue
|
|
238
|
+
key, value = token.split("=", 1)
|
|
239
|
+
parsed_build_args[key.strip()] = value.strip()
|
|
240
|
+
|
|
241
|
+
return parsed_build_args or None
|
|
201
242
|
|
|
202
243
|
|
|
203
244
|
def parse_container_env_tokens(value: str | list[str] | None) -> list[str]:
|
common_python_tasks/git.py
CHANGED
|
@@ -96,7 +96,7 @@ def compute_next_release_version(
|
|
|
96
96
|
Returns:
|
|
97
97
|
A `NextReleaseVersion` with the serialized new version and normalized component.
|
|
98
98
|
"""
|
|
99
|
-
current_version_text = project.
|
|
99
|
+
current_version_text = project.get_project_version()
|
|
100
100
|
release_component = (
|
|
101
101
|
infer_bump_component_from_git_cliff() if component is None else component
|
|
102
102
|
)
|
|
@@ -181,14 +181,11 @@ def infer_bump_component_from_git_cliff() -> ReleaseComponent:
|
|
|
181
181
|
except Exception:
|
|
182
182
|
utils.fatal(f"Unable to parse bumped version from git-cliff: {version_text!r}")
|
|
183
183
|
|
|
184
|
-
current_version = project.
|
|
184
|
+
current_version = project.get_project_version()
|
|
185
185
|
try:
|
|
186
186
|
current = Version.parse(current_version)
|
|
187
187
|
except Exception:
|
|
188
|
-
utils.fatal(
|
|
189
|
-
"Unable to parse current version from Poetry output: "
|
|
190
|
-
f"{current_version!r}"
|
|
191
|
-
)
|
|
188
|
+
utils.fatal("Unable to parse current project version: " f"{current_version!r}")
|
|
192
189
|
|
|
193
190
|
def _parse_semver(version_value: str | Any) -> tuple[int, int, int]:
|
|
194
191
|
if not isinstance(version_value, str):
|
|
@@ -220,6 +217,20 @@ def infer_bump_component_from_git_cliff() -> ReleaseComponent:
|
|
|
220
217
|
)
|
|
221
218
|
|
|
222
219
|
|
|
220
|
+
def git_available() -> bool:
|
|
221
|
+
"""Return `True` if Git is installed and the current directory is a repository."""
|
|
222
|
+
|
|
223
|
+
if shutil.which("git") is None:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
result = utils.run_command(
|
|
227
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
228
|
+
capture_output=True,
|
|
229
|
+
acceptable_returncodes={0, 1, 128},
|
|
230
|
+
)
|
|
231
|
+
return result.returncode == 0
|
|
232
|
+
|
|
233
|
+
|
|
223
234
|
def get_dirty_files(ignore: list[Path] | None = None) -> list[Path]:
|
|
224
235
|
"""Get dirty git files excluding any specified paths.
|
|
225
236
|
|
|
@@ -233,6 +244,9 @@ def get_dirty_files(ignore: list[Path] | None = None) -> list[Path]:
|
|
|
233
244
|
if ignore is None:
|
|
234
245
|
ignore = []
|
|
235
246
|
|
|
247
|
+
if not git_available():
|
|
248
|
+
return []
|
|
249
|
+
|
|
236
250
|
ignore_set = {Path(p) for p in ignore}
|
|
237
251
|
|
|
238
252
|
return [
|
|
@@ -260,6 +274,10 @@ def get_version(ignore: list[Path] | None = None) -> str:
|
|
|
260
274
|
if ignore is None:
|
|
261
275
|
ignore = []
|
|
262
276
|
|
|
277
|
+
if not git_available():
|
|
278
|
+
LOGGER.warning("Git is unavailable. Falling back to placeholder version 0.0.0.")
|
|
279
|
+
return "0.0.0"
|
|
280
|
+
|
|
263
281
|
dirty_files = get_dirty_files(ignore=ignore)
|
|
264
282
|
LOGGER.debug("Dirty files: %s", dirty_files)
|
|
265
283
|
|
|
@@ -296,6 +314,9 @@ def has_tags_later_in_history() -> bool:
|
|
|
296
314
|
`True` if tags are later in history, else `False`.
|
|
297
315
|
"""
|
|
298
316
|
|
|
317
|
+
if not git_available():
|
|
318
|
+
return False
|
|
319
|
+
|
|
299
320
|
result = utils.run_command(
|
|
300
321
|
["git", "tag"],
|
|
301
322
|
capture_output=True,
|