common-python-tasks 0.8.0__py3-none-any.whl → 0.10.0__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/__init__.py +7 -3
- common_python_tasks/data/fastapi/compose-base.yml.j2 +1 -2
- common_python_tasks/data/fastapi/compose-db.yml.j2 +1 -2
- common_python_tasks/data/fastapi/compose-debug.yml.j2 +1 -1
- common_python_tasks/data/generic/.dockerignore +1 -1
- common_python_tasks/data/generic/Dockerfile.deps.j2 +6 -1
- common_python_tasks/data/generic/Dockerfile.j2 +13 -39
- common_python_tasks/data/generic/ruff.toml +1 -1
- common_python_tasks/docker.py +7 -24
- common_python_tasks/docker_compose.py +17 -20
- common_python_tasks/env.py +89 -2
- common_python_tasks/git.py +2 -2
- common_python_tasks/github.py +20 -1
- common_python_tasks/project.py +127 -297
- common_python_tasks/tasks.py +28 -18
- common_python_tasks/utils.py +14 -6
- {common_python_tasks-0.8.0.dist-info → common_python_tasks-0.10.0.dist-info}/METADATA +35 -25
- common_python_tasks-0.10.0.dist-info/RECORD +27 -0
- common_python_tasks-0.8.0.dist-info/RECORD +0 -27
- {common_python_tasks-0.8.0.dist-info → common_python_tasks-0.10.0.dist-info}/WHEEL +0 -0
- {common_python_tasks-0.8.0.dist-info → common_python_tasks-0.10.0.dist-info}/licenses/LICENSE +0 -0
common_python_tasks/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
if TYPE_CHECKING:
|
|
4
|
-
from
|
|
4
|
+
from collections.abc import Sequence
|
|
5
5
|
|
|
6
6
|
from poethepoet_tasks import TaskCollection
|
|
7
7
|
|
|
@@ -9,7 +9,8 @@ __all__ = ["TaskCollection"]
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def tasks(
|
|
12
|
-
include_tags: "Sequence[str] | None" = None,
|
|
12
|
+
include_tags: "Sequence[str] | None" = None,
|
|
13
|
+
exclude_tags: "Sequence[str] | None" = None,
|
|
13
14
|
) -> dict:
|
|
14
15
|
"""Return the task collection filtered by the requested tags.
|
|
15
16
|
|
|
@@ -25,6 +26,9 @@ def tasks(
|
|
|
25
26
|
if include_tags is None and not exclude_tags:
|
|
26
27
|
include_tags = ("common",)
|
|
27
28
|
elif include_tags is None:
|
|
28
|
-
include_tags =
|
|
29
|
+
include_tags = ()
|
|
30
|
+
|
|
31
|
+
if exclude_tags is None:
|
|
32
|
+
exclude_tags = ()
|
|
29
33
|
|
|
30
34
|
return task_collection(include_tags=include_tags, exclude_tags=exclude_tags)
|
|
@@ -9,8 +9,7 @@ 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
|
-
|
|
13
|
-
PACKAGE_MANAGER_VERSION: ${PACKAGE_MANAGER_VERSION}
|
|
12
|
+
UV_VERSION: ${UV_VERSION}
|
|
14
13
|
|
|
15
14
|
services:
|
|
16
15
|
api:
|
|
@@ -8,8 +8,7 @@ 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
|
-
|
|
12
|
-
PACKAGE_MANAGER_VERSION: ${PACKAGE_MANAGER_VERSION}
|
|
11
|
+
UV_VERSION: ${UV_VERSION}
|
|
13
12
|
WORKDIR_PATH: ${WORKDIR_PATH}
|
|
14
13
|
|
|
15
14
|
services:
|
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
ARG PYTHON_VERSION=3
|
|
6
6
|
ARG PYTHON_VARIANT=slim
|
|
7
7
|
|
|
8
|
-
FROM python:${PYTHON_VERSION}
|
|
8
|
+
FROM python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}} AS deps
|
|
9
|
+
ARG PYTHON_VERSION
|
|
10
|
+
ARG PYTHON_VARIANT
|
|
11
|
+
|
|
12
|
+
LABEL org.opencontainers.image.base.name="python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}}" \
|
|
13
|
+
org.opencontainers.image.python.variant="${PYTHON_VARIANT}"
|
|
9
14
|
|
|
10
15
|
ARG CACHE_ID_SUFFIX
|
|
11
16
|
RUN printf '%s' "$CACHE_ID_SUFFIX" >/tmp/.cache_id_suffix
|
|
@@ -4,36 +4,12 @@
|
|
|
4
4
|
# the latest. The `build-image` task passes the version properly here
|
|
5
5
|
ARG PYTHON_VERSION=3
|
|
6
6
|
# Variant for the runtime image, e.g. slim, alpine, etc.
|
|
7
|
-
# See https://hub.docker.com/_/python for available variants
|
|
7
|
+
# See https://hub.docker.com/_/python for available variants. Leave empty for no variant.
|
|
8
8
|
ARG PYTHON_VARIANT=slim
|
|
9
|
-
ARG WORKDIR_PATH=/workspace
|
|
10
|
-
{% set package_manager = PACKAGE_MANAGER|default('poetry') %}
|
|
11
9
|
|
|
12
10
|
FROM python:${PYTHON_VERSION} AS builder
|
|
13
11
|
|
|
14
12
|
ARG PACKAGE_VERSION
|
|
15
|
-
|
|
16
|
-
{% if package_manager == 'poetry' %}
|
|
17
|
-
ARG POETRY_VERSION
|
|
18
|
-
ARG POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION
|
|
19
|
-
ARG POETRY_PLUGIN_EXPORT_VERSION
|
|
20
|
-
|
|
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
13
|
ARG UV_VERSION
|
|
38
14
|
|
|
39
15
|
# Bypass VCS-based version detection: use the host-computed version directly.
|
|
@@ -48,7 +24,6 @@ RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }}
|
|
|
48
24
|
else \
|
|
49
25
|
pip install --root-user-action=ignore uv; \
|
|
50
26
|
fi'
|
|
51
|
-
{% endif %}
|
|
52
27
|
|
|
53
28
|
# Build package
|
|
54
29
|
WORKDIR /tmp/build
|
|
@@ -56,28 +31,27 @@ COPY . /tmp/build/
|
|
|
56
31
|
|
|
57
32
|
{% if HAS_DEBUG_DEPS %}
|
|
58
33
|
# Export debug requirements when the project defines a debug dependency group
|
|
59
|
-
{%
|
|
60
|
-
|
|
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 }} \
|
|
34
|
+
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }}{% for mount in UV_INDEX_SECRET_MOUNTS %} \
|
|
35
|
+
--mount={{ mount }}{% endfor %} \
|
|
64
36
|
uv export --group debug --no-hashes --format requirements-txt --output-file requirements-debug.txt
|
|
65
37
|
{% endif %}
|
|
66
|
-
{% endif %}
|
|
67
38
|
|
|
68
|
-
# Build the wheel, caching the
|
|
39
|
+
# Build the wheel, caching the uv cache directory to speed up subsequent builds.
|
|
69
40
|
# If running this in a CI system, you must be using a persistent builder to take
|
|
70
41
|
# advantage of the cache mount, and you should configure your CI to persist the
|
|
71
42
|
# cache directory between builds
|
|
72
|
-
{%
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }} uv build --wheel
|
|
76
|
-
{% endif %}
|
|
43
|
+
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }}{% for mount in UV_INDEX_SECRET_MOUNTS %} \
|
|
44
|
+
--mount={{ mount }}{% endfor %} \
|
|
45
|
+
uv build --wheel
|
|
77
46
|
|
|
78
|
-
FROM python:${PYTHON_VERSION}
|
|
47
|
+
FROM python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}} AS runtime
|
|
48
|
+
ARG PYTHON_VERSION
|
|
49
|
+
ARG PYTHON_VARIANT
|
|
79
50
|
ARG WORKDIR_PATH=/workspace
|
|
80
51
|
|
|
52
|
+
LABEL org.opencontainers.image.base.name="python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}}" \
|
|
53
|
+
org.opencontainers.image.python.variant="${PYTHON_VARIANT}"
|
|
54
|
+
|
|
81
55
|
ENV PYTHONUNBUFFERED=1
|
|
82
56
|
ENV PYTHONDONTWRITEBYTECODE=1
|
|
83
57
|
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
line-length = 88
|
|
2
|
-
extend-exclude = [".dist", "
|
|
2
|
+
extend-exclude = [".dist", "build"]
|
common_python_tasks/docker.py
CHANGED
|
@@ -3,14 +3,14 @@ import logging
|
|
|
3
3
|
import platform
|
|
4
4
|
import re
|
|
5
5
|
import tempfile
|
|
6
|
+
from collections.abc import Sequence
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from enum import IntEnum, StrEnum
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from shlex import quote
|
|
10
|
-
from typing import Sequence
|
|
11
11
|
|
|
12
12
|
from . import git, project, utils
|
|
13
|
-
from .env import get_cache_id_suffix, get_workdir_path
|
|
13
|
+
from .env import get_cache_id_suffix, get_python_variant, get_workdir_path
|
|
14
14
|
|
|
15
15
|
LOGGER = logging.getLogger(__name__)
|
|
16
16
|
|
|
@@ -289,37 +289,17 @@ def render_build_image(
|
|
|
289
289
|
commit_tag = version_tag
|
|
290
290
|
|
|
291
291
|
python_version = platform.python_version()
|
|
292
|
-
package_manager = project.get_package_manager()
|
|
293
|
-
package_manager_version = project.get_package_manager_version(package_manager)
|
|
294
|
-
|
|
295
|
-
manager_build_args: dict[str, str | None]
|
|
296
|
-
if package_manager == project.PackageManager.POETRY:
|
|
297
|
-
manager_build_args = {
|
|
298
|
-
"POETRY_VERSION": package_manager_version,
|
|
299
|
-
"POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION": project.get_installed_requirement_version(
|
|
300
|
-
"poetry-dynamic-versioning[plugin]"
|
|
301
|
-
),
|
|
302
|
-
"POETRY_PLUGIN_EXPORT_VERSION": project.get_installed_requirement_version(
|
|
303
|
-
"poetry-plugin-export"
|
|
304
|
-
),
|
|
305
|
-
}
|
|
306
|
-
else:
|
|
307
|
-
manager_build_args = {
|
|
308
|
-
"UV_VERSION": package_manager_version,
|
|
309
|
-
}
|
|
310
292
|
|
|
311
293
|
build_args = _merge_build_args(
|
|
312
294
|
{
|
|
313
295
|
"PYTHON_VERSION": python_version,
|
|
314
|
-
"
|
|
315
|
-
"PACKAGE_MANAGER_VERSION": package_manager_version,
|
|
296
|
+
"UV_VERSION": project.get_uv_version(),
|
|
316
297
|
"PACKAGE_VERSION": package_version,
|
|
317
298
|
"PACKAGE_NAME": utils.get_package_name(use_underscores=True),
|
|
318
299
|
"AUTHORS": ",".join(
|
|
319
300
|
[f"{name} <{email}>" for name, email in project.get_authors()]
|
|
320
301
|
),
|
|
321
302
|
"GIT_COMMIT": commit_tag,
|
|
322
|
-
**manager_build_args,
|
|
323
303
|
},
|
|
324
304
|
extra_build_args,
|
|
325
305
|
)
|
|
@@ -580,7 +560,10 @@ def build_deps_image(
|
|
|
580
560
|
archs = _resolve_archs(single_arch)
|
|
581
561
|
|
|
582
562
|
build_args = _merge_build_args(
|
|
583
|
-
{
|
|
563
|
+
{
|
|
564
|
+
"PYTHON_VERSION": python_version,
|
|
565
|
+
"PYTHON_VARIANT": get_python_variant(),
|
|
566
|
+
},
|
|
584
567
|
extra_build_args,
|
|
585
568
|
)
|
|
586
569
|
if cache_id_suffix:
|
|
@@ -5,16 +5,17 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
import platform
|
|
7
7
|
import tempfile
|
|
8
|
+
from collections.abc import Sequence
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from shlex import quote
|
|
10
|
-
from typing import NoReturn
|
|
11
|
+
from typing import NoReturn
|
|
11
12
|
|
|
12
13
|
from jinja2 import Template
|
|
13
14
|
from poethepoet_tasks import TaskCollection
|
|
14
15
|
|
|
15
16
|
from . import utils
|
|
16
17
|
from .env import get_workdir_path
|
|
17
|
-
from .project import
|
|
18
|
+
from .project import get_uv_version
|
|
18
19
|
|
|
19
20
|
LOGGER = logging.getLogger(__name__)
|
|
20
21
|
|
|
@@ -113,16 +114,15 @@ def render_file(
|
|
|
113
114
|
if extra_template_vars:
|
|
114
115
|
template_vars.update(extra_template_vars)
|
|
115
116
|
rendered = Template(path_obj.read_text()).render(**template_vars)
|
|
116
|
-
|
|
117
|
+
with tempfile.NamedTemporaryFile(
|
|
117
118
|
mode="w",
|
|
118
119
|
encoding="utf-8",
|
|
119
120
|
delete=False,
|
|
120
121
|
prefix=path_obj.stem + ".",
|
|
121
122
|
suffix=suffix,
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
tf.close()
|
|
123
|
+
) as temp_file:
|
|
124
|
+
temp_path = Path(temp_file.name)
|
|
125
|
+
temp_file.write(rendered)
|
|
126
126
|
return temp_path, True
|
|
127
127
|
|
|
128
128
|
return path_obj, False
|
|
@@ -154,7 +154,7 @@ def read_dotenv(path: Path, error_on_fail: bool = False) -> dict[str, str]:
|
|
|
154
154
|
value = value.strip().strip('"').strip("'")
|
|
155
155
|
if key:
|
|
156
156
|
env[key] = value
|
|
157
|
-
except
|
|
157
|
+
except (OSError, UnicodeError) as exc:
|
|
158
158
|
if error_on_fail:
|
|
159
159
|
utils.fatal(f"Failed to parse .env file {path}: {exc}")
|
|
160
160
|
else:
|
|
@@ -169,9 +169,9 @@ def append_dotenv(path: Path, items: dict[str, str]) -> None:
|
|
|
169
169
|
path: Path to the `.env` file.
|
|
170
170
|
items: Environment variables to append to the file.
|
|
171
171
|
"""
|
|
172
|
-
from datetime import datetime
|
|
172
|
+
from datetime import UTC, datetime
|
|
173
173
|
|
|
174
|
-
header = f"\n# Auto-generated by common_python_tasks on {datetime.now().isoformat(timespec='seconds')}\n"
|
|
174
|
+
header = f"\n# Auto-generated by common_python_tasks on {datetime.now(UTC).astimezone().isoformat(timespec='seconds')}\n"
|
|
175
175
|
with path.open("a", encoding="utf-8") as f:
|
|
176
176
|
f.write(header)
|
|
177
177
|
for k, v in items.items():
|
|
@@ -214,7 +214,7 @@ def get_or_generate_secret(
|
|
|
214
214
|
try:
|
|
215
215
|
append_dotenv(dotenv_path, {key_name: token})
|
|
216
216
|
LOGGER.info("Generated %s and stored it in .env", key_name)
|
|
217
|
-
except
|
|
217
|
+
except OSError as exc:
|
|
218
218
|
LOGGER.warning(
|
|
219
219
|
"Failed to persist %s to .env (%s); using in-memory only", key_name, exc
|
|
220
220
|
)
|
|
@@ -239,8 +239,7 @@ _COMPOSE_VAR_REQUIREMENTS: dict[str, dict[str, set[str]]] = {
|
|
|
239
239
|
"ENVIRONMENT",
|
|
240
240
|
"IMAGE_TAG",
|
|
241
241
|
"PYTHON_VERSION",
|
|
242
|
-
"
|
|
243
|
-
"PACKAGE_MANAGER_VERSION",
|
|
242
|
+
"UV_VERSION",
|
|
244
243
|
},
|
|
245
244
|
"compose-db": {
|
|
246
245
|
"PACKAGE_NAME",
|
|
@@ -250,8 +249,7 @@ _COMPOSE_VAR_REQUIREMENTS: dict[str, dict[str, set[str]]] = {
|
|
|
250
249
|
"DB_PORT",
|
|
251
250
|
"IMAGE_TAG",
|
|
252
251
|
"PYTHON_VERSION",
|
|
253
|
-
"
|
|
254
|
-
"PACKAGE_MANAGER_VERSION",
|
|
252
|
+
"UV_VERSION",
|
|
255
253
|
"POSTGRES_VERSION",
|
|
256
254
|
"WORKDIR_PATH",
|
|
257
255
|
},
|
|
@@ -337,13 +335,12 @@ def get_compose_env(
|
|
|
337
335
|
"ENVIRONMENT": os.getenv("ENVIRONMENT", "production"),
|
|
338
336
|
"IMAGE_TAG": image_tag or "latest",
|
|
339
337
|
"PACKAGE_NAME": package_name,
|
|
340
|
-
"PACKAGE_MANAGER": get_package_manager(),
|
|
341
|
-
"PACKAGE_MANAGER_VERSION": get_package_manager_version(),
|
|
342
338
|
"PACKAGE_UNDERSCORE_NAME": utils.package_name_to_underscore(package_name),
|
|
343
339
|
"WORKDIR_PATH": get_workdir_path(),
|
|
344
340
|
"POSTGRES_VERSION": os.getenv("POSTGRES_VERSION", "17"),
|
|
345
341
|
"PYTHON_VERSION": platform.python_version(),
|
|
346
342
|
"SECRET_KEY": os.getenv("SECRET_KEY", ""),
|
|
343
|
+
"UV_VERSION": get_uv_version(),
|
|
347
344
|
}
|
|
348
345
|
|
|
349
346
|
if compose_type and compose_files:
|
|
@@ -593,14 +590,14 @@ def build_exec_script(
|
|
|
593
590
|
Returns:
|
|
594
591
|
Absolute path to the temporary script file.
|
|
595
592
|
"""
|
|
596
|
-
|
|
593
|
+
with tempfile.NamedTemporaryFile(
|
|
597
594
|
mode="w",
|
|
598
595
|
encoding="utf-8",
|
|
599
596
|
delete=False,
|
|
600
597
|
prefix="compose-exec.",
|
|
601
598
|
suffix=".sh",
|
|
602
|
-
)
|
|
603
|
-
|
|
599
|
+
) as temp_file:
|
|
600
|
+
script_path = Path(temp_file.name)
|
|
604
601
|
script_path_str = str(script_path.resolve())
|
|
605
602
|
|
|
606
603
|
lines = [
|
common_python_tasks/env.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
|
+
import re
|
|
3
4
|
import secrets
|
|
4
5
|
import shlex
|
|
5
6
|
from pathlib import Path
|
|
@@ -21,6 +22,82 @@ TRUTHY_VALUES = {
|
|
|
21
22
|
WORKDIR_PATH_ENV_VAR = "WORKDIR_PATH"
|
|
22
23
|
WORKDIR_PATH_DEFAULT = "/workspace"
|
|
23
24
|
_AUTO_INJECTED_BUILD_ARG_ENV_VARS = (WORKDIR_PATH_ENV_VAR,)
|
|
25
|
+
_UV_INDEX_CREDENTIAL_PATTERN = re.compile(r"^UV_INDEX_(.+)_(USERNAME|PASSWORD)$")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def collect_uv_index_credentials() -> list[dict[str, str | None]]:
|
|
29
|
+
"""Collect present `UV_INDEX_{name}_{USERNAME,PASSWORD}` host env vars.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A list of dicts with keys `index_name`, `username_env`, and
|
|
33
|
+
`password_env`. Only indices with at least one credential set are
|
|
34
|
+
included, sorted by index name.
|
|
35
|
+
"""
|
|
36
|
+
found = {}
|
|
37
|
+
for key in os.environ:
|
|
38
|
+
match = _UV_INDEX_CREDENTIAL_PATTERN.match(key)
|
|
39
|
+
if match:
|
|
40
|
+
found.setdefault(match.group(1), set()).add(match.group(2))
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
"index_name": name,
|
|
45
|
+
"username_env": f"UV_INDEX_{name}_USERNAME"
|
|
46
|
+
if "USERNAME" in found[name]
|
|
47
|
+
else None,
|
|
48
|
+
"password_env": f"UV_INDEX_{name}_PASSWORD"
|
|
49
|
+
if "PASSWORD" in found[name]
|
|
50
|
+
else None,
|
|
51
|
+
}
|
|
52
|
+
for name in sorted(found)
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def uv_index_secret_mounts(credentials: list[dict[str, str | None]]) -> list[str]:
|
|
57
|
+
"""Return Dockerfile ``--mount`` strings for UV index credentials.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
credentials: Output of `collect_uv_index_credentials`.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
A list of ``type=secret,...`` strings for Dockerfile ``RUN --mount=``
|
|
64
|
+
directives.
|
|
65
|
+
"""
|
|
66
|
+
mounts = []
|
|
67
|
+
for cred in credentials:
|
|
68
|
+
name = cred["index_name"].lower()
|
|
69
|
+
if cred["username_env"]:
|
|
70
|
+
mounts.append(
|
|
71
|
+
f"type=secret,id=uv_index_{name}_username,env={cred['username_env']}"
|
|
72
|
+
)
|
|
73
|
+
if cred["password_env"]:
|
|
74
|
+
mounts.append(
|
|
75
|
+
f"type=secret,id=uv_index_{name}_password,env={cred['password_env']}"
|
|
76
|
+
)
|
|
77
|
+
return mounts
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def uv_index_secret_build_args(credentials: list[dict[str, str | None]]) -> list[str]:
|
|
81
|
+
"""Return ``docker build --secret`` arg pairs for UV index credentials.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
credentials: Output of `collect_uv_index_credentials`.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A flat list of ``--secret`` flag-value pairs for the docker build command.
|
|
88
|
+
"""
|
|
89
|
+
args = []
|
|
90
|
+
for cred in credentials:
|
|
91
|
+
name = cred["index_name"].lower()
|
|
92
|
+
if cred["username_env"]:
|
|
93
|
+
args.extend(
|
|
94
|
+
["--secret", f"id=uv_index_{name}_username,env={cred['username_env']}"]
|
|
95
|
+
)
|
|
96
|
+
if cred["password_env"]:
|
|
97
|
+
args.extend(
|
|
98
|
+
["--secret", f"id=uv_index_{name}_password,env={cred['password_env']}"]
|
|
99
|
+
)
|
|
100
|
+
return args
|
|
24
101
|
|
|
25
102
|
|
|
26
103
|
def env_truthy(env_var: str) -> bool:
|
|
@@ -44,6 +121,16 @@ def get_workdir_path() -> str:
|
|
|
44
121
|
return os.getenv(WORKDIR_PATH_ENV_VAR, WORKDIR_PATH_DEFAULT)
|
|
45
122
|
|
|
46
123
|
|
|
124
|
+
def get_python_variant() -> str:
|
|
125
|
+
"""Return the Python image variant for container builds.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The variant from `CONTAINER_PYTHON_VARIANT`, or 'slim' by default.
|
|
129
|
+
An empty string disables the variant (e.g., `FROM python:3.11`).
|
|
130
|
+
"""
|
|
131
|
+
return os.getenv("CONTAINER_PYTHON_VARIANT", "slim").strip()
|
|
132
|
+
|
|
133
|
+
|
|
47
134
|
def inject_auto_build_args_from_env(
|
|
48
135
|
build_args: dict[str, str] | None,
|
|
49
136
|
) -> dict[str, str]:
|
|
@@ -454,7 +541,7 @@ def render_container_deps_move_script(mappings: dict[str, str]) -> str:
|
|
|
454
541
|
"import shutil",
|
|
455
542
|
"import sys",
|
|
456
543
|
"",
|
|
457
|
-
f"mappings = {
|
|
544
|
+
f"mappings = {mappings!r}",
|
|
458
545
|
'source_root = pathlib.Path("/tmp/deps")',
|
|
459
546
|
"",
|
|
460
547
|
"for name, dest in mappings.items():",
|
|
@@ -523,7 +610,7 @@ def get_prune_keep() -> int:
|
|
|
523
610
|
return -1
|
|
524
611
|
try:
|
|
525
612
|
return int(raw)
|
|
526
|
-
except
|
|
613
|
+
except ValueError:
|
|
527
614
|
LOGGER.warning(
|
|
528
615
|
"Invalid CONTAINER_PRUNE_KEEP value '%s' - defaulting to -1 (no prune)",
|
|
529
616
|
raw,
|
common_python_tasks/git.py
CHANGED
|
@@ -178,13 +178,13 @@ def infer_bump_component_from_git_cliff() -> ReleaseComponent:
|
|
|
178
178
|
normalized_version = version_text.lstrip("v")
|
|
179
179
|
try:
|
|
180
180
|
bumped = Version.parse(normalized_version)
|
|
181
|
-
except
|
|
181
|
+
except ValueError:
|
|
182
182
|
utils.fatal(f"Unable to parse bumped version from git-cliff: {version_text!r}")
|
|
183
183
|
|
|
184
184
|
current_version = project.get_project_version()
|
|
185
185
|
try:
|
|
186
186
|
current = Version.parse(current_version)
|
|
187
|
-
except
|
|
187
|
+
except ValueError:
|
|
188
188
|
utils.fatal(f"Unable to parse current project version: {current_version!r}")
|
|
189
189
|
|
|
190
190
|
def _parse_semver(version_value: str | Any) -> tuple[int, int, int]:
|
common_python_tasks/github.py
CHANGED
|
@@ -28,6 +28,18 @@ def _is_unreleased_placeholder(notes: str) -> bool:
|
|
|
28
28
|
return normalized in {"[unreleased]", "## [unreleased]", "## unreleased"}
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _pattern_targets_hidden_paths(pattern: str) -> bool:
|
|
32
|
+
return any(
|
|
33
|
+
part.startswith(".")
|
|
34
|
+
for part in pattern.split("/")
|
|
35
|
+
if part and part not in {".", ".."}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _path_has_hidden_segment(path: Path) -> bool:
|
|
40
|
+
return any(part.startswith(".") for part in path.parts if part not in {".", ".."})
|
|
41
|
+
|
|
42
|
+
|
|
31
43
|
def _latest_tagged_changelog() -> str | None:
|
|
32
44
|
result = utils.run_git_cliff(["--latest"], capture_output=True)
|
|
33
45
|
changelog = result.stdout.strip()
|
|
@@ -538,6 +550,8 @@ def get_github_release_asset_paths(
|
|
|
538
550
|
interpreted as colon-separated paths or glob patterns.
|
|
539
551
|
If not provided, the default value is `dist/*`. An empty list
|
|
540
552
|
explicitly disables asset selection.
|
|
553
|
+
Wildcard patterns only include hidden files when the pattern itself
|
|
554
|
+
explicitly targets hidden paths (for example, `dist/.*`).
|
|
541
555
|
|
|
542
556
|
Returns:
|
|
543
557
|
A sorted list of existing asset paths.
|
|
@@ -561,9 +575,14 @@ def get_github_release_asset_paths(
|
|
|
561
575
|
asset_paths = []
|
|
562
576
|
for part in source_items:
|
|
563
577
|
if any(char in part for char in "*?[]"):
|
|
578
|
+
matches = [path for path in Path(".").glob(part) if path.is_file()]
|
|
579
|
+
if not _pattern_targets_hidden_paths(part):
|
|
580
|
+
matches = [
|
|
581
|
+
path for path in matches if not _path_has_hidden_segment(path)
|
|
582
|
+
]
|
|
564
583
|
asset_paths.extend(
|
|
565
584
|
sorted(
|
|
566
|
-
|
|
585
|
+
matches,
|
|
567
586
|
key=lambda p: str(p),
|
|
568
587
|
)
|
|
569
588
|
)
|