common-python-tasks 0.9.0__py3-none-any.whl → 0.10.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/__init__.py +7 -3
- common_python_tasks/data/generic/.dockerignore +0 -1
- common_python_tasks/data/generic/Dockerfile.deps.j2 +6 -1
- common_python_tasks/data/generic/Dockerfile.j2 +12 -5
- common_python_tasks/docker.py +6 -7
- common_python_tasks/docker_compose.py +13 -13
- common_python_tasks/env.py +89 -2
- common_python_tasks/git.py +2 -2
- common_python_tasks/project.py +3 -2
- common_python_tasks/tasks.py +35 -18
- common_python_tasks/utils.py +14 -6
- {common_python_tasks-0.9.0.dist-info → common_python_tasks-0.10.1.dist-info}/METADATA +8 -6
- common_python_tasks-0.10.1.dist-info/RECORD +27 -0
- common_python_tasks-0.9.0.dist-info/RECORD +0 -27
- {common_python_tasks-0.9.0.dist-info → common_python_tasks-0.10.1.dist-info}/WHEEL +0 -0
- {common_python_tasks-0.9.0.dist-info → common_python_tasks-0.10.1.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)
|
|
@@ -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,9 +4,8 @@
|
|
|
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
9
|
|
|
11
10
|
FROM python:${PYTHON_VERSION} AS builder
|
|
12
11
|
|
|
@@ -32,7 +31,8 @@ COPY . /tmp/build/
|
|
|
32
31
|
|
|
33
32
|
{% if HAS_DEBUG_DEPS %}
|
|
34
33
|
# Export debug requirements when the project defines a debug dependency group
|
|
35
|
-
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 %} \
|
|
36
36
|
uv export --group debug --no-hashes --format requirements-txt --output-file requirements-debug.txt
|
|
37
37
|
{% endif %}
|
|
38
38
|
|
|
@@ -40,11 +40,18 @@ RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }} \
|
|
|
40
40
|
# If running this in a CI system, you must be using a persistent builder to take
|
|
41
41
|
# advantage of the cache mount, and you should configure your CI to persist the
|
|
42
42
|
# cache directory between builds
|
|
43
|
-
RUN --mount=type=cache,target=/root/.cache/uv,id=uv-cache{{ CACHE_ID_SUFFIX }}
|
|
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
|
|
44
46
|
|
|
45
|
-
FROM python:${PYTHON_VERSION}
|
|
47
|
+
FROM python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}} AS runtime
|
|
48
|
+
ARG PYTHON_VERSION
|
|
49
|
+
ARG PYTHON_VARIANT
|
|
46
50
|
ARG WORKDIR_PATH=/workspace
|
|
47
51
|
|
|
52
|
+
LABEL org.opencontainers.image.base.name="python:${PYTHON_VERSION}${PYTHON_VARIANT:+-${PYTHON_VARIANT}}" \
|
|
53
|
+
org.opencontainers.image.python.variant="${PYTHON_VARIANT}"
|
|
54
|
+
|
|
48
55
|
ENV PYTHONUNBUFFERED=1
|
|
49
56
|
ENV PYTHONDONTWRITEBYTECODE=1
|
|
50
57
|
|
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
|
|
|
@@ -205,10 +205,6 @@ def render_build_image(
|
|
|
205
205
|
Returns:
|
|
206
206
|
The rendered build plan that contains the command and generated artifacts.
|
|
207
207
|
"""
|
|
208
|
-
dist_path = Path("dist")
|
|
209
|
-
if utils.directory_has_contents(dist_path):
|
|
210
|
-
utils.remove_path(dist_path)
|
|
211
|
-
|
|
212
208
|
context_path = _resolve_context_path(context_path)
|
|
213
209
|
|
|
214
210
|
temp_file_path = None
|
|
@@ -560,7 +556,10 @@ def build_deps_image(
|
|
|
560
556
|
archs = _resolve_archs(single_arch)
|
|
561
557
|
|
|
562
558
|
build_args = _merge_build_args(
|
|
563
|
-
{
|
|
559
|
+
{
|
|
560
|
+
"PYTHON_VERSION": python_version,
|
|
561
|
+
"PYTHON_VARIANT": get_python_variant(),
|
|
562
|
+
},
|
|
564
563
|
extra_build_args,
|
|
565
564
|
)
|
|
566
565
|
if cache_id_suffix:
|
|
@@ -5,9 +5,10 @@ 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
|
|
@@ -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
|
)
|
|
@@ -590,14 +590,14 @@ def build_exec_script(
|
|
|
590
590
|
Returns:
|
|
591
591
|
Absolute path to the temporary script file.
|
|
592
592
|
"""
|
|
593
|
-
|
|
593
|
+
with tempfile.NamedTemporaryFile(
|
|
594
594
|
mode="w",
|
|
595
595
|
encoding="utf-8",
|
|
596
596
|
delete=False,
|
|
597
597
|
prefix="compose-exec.",
|
|
598
598
|
suffix=".sh",
|
|
599
|
-
)
|
|
600
|
-
|
|
599
|
+
) as temp_file:
|
|
600
|
+
script_path = Path(temp_file.name)
|
|
601
601
|
script_path_str = str(script_path.resolve())
|
|
602
602
|
|
|
603
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/project.py
CHANGED
|
@@ -3,10 +3,11 @@ import logging
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
5
|
import tomllib
|
|
6
|
+
from collections.abc import Sequence
|
|
6
7
|
from functools import lru_cache
|
|
7
8
|
from importlib import metadata
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
10
11
|
|
|
11
12
|
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
12
13
|
from packaging.version import Version
|
|
@@ -513,7 +514,7 @@ def get_project_version_from_vcs() -> str | None:
|
|
|
513
514
|
from dunamai import Style, Version
|
|
514
515
|
|
|
515
516
|
return Version.from_git().serialize(style=Style.Pep440, dirty=False)
|
|
516
|
-
except
|
|
517
|
+
except (OSError, RuntimeError, ValueError):
|
|
517
518
|
return None
|
|
518
519
|
|
|
519
520
|
|
common_python_tasks/tasks.py
CHANGED
|
@@ -4,9 +4,10 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import shlex
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from functools import partial, wraps
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
10
|
+
from typing import ClassVar, NamedTuple
|
|
10
11
|
|
|
11
12
|
from poethepoet_tasks import TaskCollection
|
|
12
13
|
|
|
@@ -14,7 +15,7 @@ from common_python_tasks.utils import confirm
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class _ColoredFormatter(logging.Formatter):
|
|
17
|
-
COLORS = {
|
|
18
|
+
COLORS: ClassVar = {
|
|
18
19
|
"WARNING": "\033[93m",
|
|
19
20
|
"ERROR": "\033[91m",
|
|
20
21
|
"CRITICAL": "\033[91m",
|
|
@@ -445,9 +446,11 @@ def build_image(
|
|
|
445
446
|
prune_images_keep,
|
|
446
447
|
)
|
|
447
448
|
from .env import (
|
|
449
|
+
collect_uv_index_credentials,
|
|
448
450
|
get_cache_id_suffix,
|
|
449
451
|
get_container_deps_move_script,
|
|
450
452
|
get_prune_keep,
|
|
453
|
+
get_python_variant,
|
|
451
454
|
inject_auto_build_args_from_env,
|
|
452
455
|
load_container_env_tokens,
|
|
453
456
|
parse_container_deps_mappings,
|
|
@@ -457,6 +460,8 @@ def build_image(
|
|
|
457
460
|
resolve_container_docker_build_args,
|
|
458
461
|
resolve_container_dockerfile_hook_path,
|
|
459
462
|
resolve_extension_content,
|
|
463
|
+
uv_index_secret_build_args,
|
|
464
|
+
uv_index_secret_mounts,
|
|
460
465
|
)
|
|
461
466
|
from .project import (
|
|
462
467
|
has_debug_dependency_group,
|
|
@@ -486,6 +491,15 @@ def build_image(
|
|
|
486
491
|
docker_build_args,
|
|
487
492
|
os.getenv("CONTAINER_DOCKER_BUILD_ARGS"),
|
|
488
493
|
)
|
|
494
|
+
uv_credentials = collect_uv_index_credentials()
|
|
495
|
+
if uv_credentials:
|
|
496
|
+
LOGGER.debug(
|
|
497
|
+
"Passing UV index credentials for: %s",
|
|
498
|
+
", ".join(c["index_name"] for c in uv_credentials),
|
|
499
|
+
)
|
|
500
|
+
resolved_docker_build_args = uv_index_secret_build_args(uv_credentials) + list(
|
|
501
|
+
resolved_docker_build_args
|
|
502
|
+
)
|
|
489
503
|
resolved_dockerfile_hook_path = resolve_container_dockerfile_hook_path(
|
|
490
504
|
dockerfile_hook_path,
|
|
491
505
|
os.getenv("CONTAINER_DOCKERFILE_HOOK_PATH"),
|
|
@@ -513,7 +527,9 @@ def build_image(
|
|
|
513
527
|
"Injecting builder-stage env vars: %s",
|
|
514
528
|
", ".join(container_env_vars),
|
|
515
529
|
)
|
|
516
|
-
top_level_build_args = inject_auto_build_args_from_env(
|
|
530
|
+
top_level_build_args = inject_auto_build_args_from_env(
|
|
531
|
+
{"PYTHON_VARIANT": get_python_variant()}
|
|
532
|
+
)
|
|
517
533
|
external_deps_image = _get_container_build_setting("CONTAINER_DEPS_IMAGE")
|
|
518
534
|
if external_deps_image:
|
|
519
535
|
top_level_build_args["CONTAINER_DEPS_IMAGE"] = external_deps_image
|
|
@@ -534,7 +550,7 @@ def build_image(
|
|
|
534
550
|
continue
|
|
535
551
|
|
|
536
552
|
arg_names = re.findall(
|
|
537
|
-
r"^\s*ARG\s+([A-Za-z_][A-Za-z0-9_]*)", fragment, re.
|
|
553
|
+
r"^\s*ARG\s+([A-Za-z_][A-Za-z0-9_]*)", fragment, re.MULTILINE
|
|
538
554
|
)
|
|
539
555
|
if not arg_names:
|
|
540
556
|
LOGGER.warning(
|
|
@@ -606,6 +622,7 @@ def build_image(
|
|
|
606
622
|
"ENTRYPOINT_COMMAND": entrypoint_command,
|
|
607
623
|
"CONTAINER_ENV_VARS": container_env_vars,
|
|
608
624
|
"CACHE_ID_SUFFIX": cache_id_suffix,
|
|
625
|
+
"UV_INDEX_SECRET_MOUNTS": uv_index_secret_mounts(uv_credentials),
|
|
609
626
|
},
|
|
610
627
|
)
|
|
611
628
|
|
|
@@ -1313,6 +1330,9 @@ def _run_release_flow(
|
|
|
1313
1330
|
repository,
|
|
1314
1331
|
repository_url,
|
|
1315
1332
|
)
|
|
1333
|
+
log_dry_run(
|
|
1334
|
+
"Would publish GitHub Release %s with built assets", hook_env["RELEASE_TAG"]
|
|
1335
|
+
)
|
|
1316
1336
|
if include_containers:
|
|
1317
1337
|
log_dry_run(
|
|
1318
1338
|
"Would build container image with debug=%s no_cache=%s plain=%s single_arch=%s",
|
|
@@ -1322,9 +1342,6 @@ def _run_release_flow(
|
|
|
1322
1342
|
single_arch,
|
|
1323
1343
|
)
|
|
1324
1344
|
log_dry_run("Would push container image with debug=%s", debug)
|
|
1325
|
-
log_dry_run(
|
|
1326
|
-
"Would publish GitHub Release %s with built assets", hook_env["RELEASE_TAG"]
|
|
1327
|
-
)
|
|
1328
1345
|
if post_script:
|
|
1329
1346
|
run_command(["sh", "-lc", post_script], env=hook_env, dry_run=False)
|
|
1330
1347
|
return
|
|
@@ -1333,17 +1350,6 @@ def _run_release_flow(
|
|
|
1333
1350
|
hook_env["RELEASE_TAG"], repository=repository, repository_url=repository_url
|
|
1334
1351
|
)
|
|
1335
1352
|
|
|
1336
|
-
if include_containers:
|
|
1337
|
-
build_image(
|
|
1338
|
-
debug=debug,
|
|
1339
|
-
no_cache=no_cache,
|
|
1340
|
-
plain=plain,
|
|
1341
|
-
single_arch=single_arch,
|
|
1342
|
-
container_env=container_env,
|
|
1343
|
-
container_envfile=container_envfile,
|
|
1344
|
-
)
|
|
1345
|
-
push_image(debug=debug)
|
|
1346
|
-
|
|
1347
1353
|
run_command(["git", "push", "origin", hook_env["RELEASE_TAG"]])
|
|
1348
1354
|
try:
|
|
1349
1355
|
publish_github_release(
|
|
@@ -1362,6 +1368,17 @@ def _run_release_flow(
|
|
|
1362
1368
|
)
|
|
1363
1369
|
raise
|
|
1364
1370
|
|
|
1371
|
+
if include_containers:
|
|
1372
|
+
build_image(
|
|
1373
|
+
debug=debug,
|
|
1374
|
+
no_cache=no_cache,
|
|
1375
|
+
plain=plain,
|
|
1376
|
+
single_arch=single_arch,
|
|
1377
|
+
container_env=container_env,
|
|
1378
|
+
container_envfile=container_envfile,
|
|
1379
|
+
)
|
|
1380
|
+
push_image(debug=debug)
|
|
1381
|
+
|
|
1365
1382
|
if post_script:
|
|
1366
1383
|
run_command(["sh", "-lc", post_script], env=hook_env, dry_run=False)
|
|
1367
1384
|
|
common_python_tasks/utils.py
CHANGED
|
@@ -6,12 +6,12 @@ import shutil
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
8
|
import tomllib
|
|
9
|
+
from collections.abc import Collection, Sequence
|
|
9
10
|
from functools import lru_cache
|
|
10
11
|
from getpass import getuser
|
|
11
12
|
from importlib.resources import files
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from shlex import quote
|
|
14
|
-
from typing import Collection, Sequence
|
|
15
15
|
|
|
16
16
|
from jinja2 import Template
|
|
17
17
|
|
|
@@ -154,7 +154,11 @@ def run_command(
|
|
|
154
154
|
merged_env = {**os.environ, **env} if env is not None else None
|
|
155
155
|
|
|
156
156
|
out = subprocess.run(
|
|
157
|
-
command,
|
|
157
|
+
command,
|
|
158
|
+
capture_output=capture_output,
|
|
159
|
+
check=False,
|
|
160
|
+
text=True,
|
|
161
|
+
env=merged_env,
|
|
158
162
|
)
|
|
159
163
|
if out.returncode not in acceptable_returncodes:
|
|
160
164
|
if capture_output:
|
|
@@ -234,8 +238,7 @@ def normalize_registry(registry: str | None) -> str | None:
|
|
|
234
238
|
return None
|
|
235
239
|
|
|
236
240
|
for prefix in ("https://", "http://"):
|
|
237
|
-
|
|
238
|
-
registry = registry[len(prefix) :]
|
|
241
|
+
registry = registry.removeprefix(prefix)
|
|
239
242
|
|
|
240
243
|
return registry.rstrip("/")
|
|
241
244
|
|
|
@@ -407,8 +410,13 @@ def get_package_name(use_underscores: bool = False) -> str | None:
|
|
|
407
410
|
name = (
|
|
408
411
|
tomllib.loads(pyproject.read_text()).get("project", {}).get("name")
|
|
409
412
|
)
|
|
410
|
-
except
|
|
411
|
-
|
|
413
|
+
except (
|
|
414
|
+
AttributeError,
|
|
415
|
+
OSError,
|
|
416
|
+
UnicodeDecodeError,
|
|
417
|
+
tomllib.TOMLDecodeError,
|
|
418
|
+
) as error: # pragma: no cover - defensive logging
|
|
419
|
+
LOGGER.debug("Unable to read pyproject.toml: %s", error)
|
|
412
420
|
|
|
413
421
|
if use_underscores and name:
|
|
414
422
|
name = package_name_to_underscore(name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: common-python-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.1
|
|
4
4
|
Summary: Opinionated Poe the Poet tasks for Python package development.
|
|
5
5
|
Project-URL: Homepage, http://github.com/ci-sourcerer/common-python-tasks
|
|
6
6
|
Project-URL: Source, http://github.com/ci-sourcerer/common-python-tasks.git
|
|
@@ -43,7 +43,7 @@ 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.10.1
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Configure Poe the Poet to expose the default `common` task set.
|
|
@@ -68,7 +68,7 @@ The helper script performs the same development-dependency installation and Poe
|
|
|
68
68
|
```shell
|
|
69
69
|
curl --fail --silent --show-error --location \
|
|
70
70
|
--output /tmp/add_common_python_tasks.py \
|
|
71
|
-
https://raw.githubusercontent.com/ci-sourcerer/common-python-tasks/v0.
|
|
71
|
+
https://raw.githubusercontent.com/ci-sourcerer/common-python-tasks/v0.10.1/scripts/add_common_python_tasks.py
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Review the downloaded script before executing it.
|
|
@@ -80,10 +80,10 @@ less /tmp/add_common_python_tasks.py
|
|
|
80
80
|
Run the reviewed script with the same pinned package version.
|
|
81
81
|
|
|
82
82
|
```shell
|
|
83
|
-
COMMON_PYTHON_TASKS_VERSION=0.
|
|
83
|
+
COMMON_PYTHON_TASKS_VERSION=0.10.1 python3 /tmp/add_common_python_tasks.py
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
To install another release, replace both occurrences of `0.
|
|
86
|
+
To install another release, replace both occurrences of `0.10.1` with that release's version.
|
|
87
87
|
|
|
88
88
|
## Available tasks
|
|
89
89
|
|
|
@@ -203,7 +203,7 @@ After installing the package, a minimal project configuration looks like this.
|
|
|
203
203
|
```toml
|
|
204
204
|
[project]
|
|
205
205
|
name = "simple-cli-tool"
|
|
206
|
-
version = "0.
|
|
206
|
+
version = "0.10.1"
|
|
207
207
|
|
|
208
208
|
[tool.poe]
|
|
209
209
|
include_script = "common_python_tasks:tasks()"
|
|
@@ -248,6 +248,7 @@ addopts = "-ra"
|
|
|
248
248
|
|
|
249
249
|
- `CONTAINER_REGISTRY_USERNAME`: Container-registry username for image tagging; the default is the current local user
|
|
250
250
|
- `CONTAINER_REGISTRY_URL`: Registry URL with a default of `docker.io/{username}`
|
|
251
|
+
- `CONTAINER_PYTHON_VARIANT`: Python base-image variant such as `slim`, `alpine`, etc. See <https://hub.docker.com/_/python> for available options. Defaults to `slim`; set to empty string for no variant (e.g., `FROM python:3.11`). The value is passed as the `PYTHON_VARIANT` Docker build argument and recorded in the `org.opencontainers.image.python.variant` image label.
|
|
251
252
|
- `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.
|
|
252
253
|
- `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.
|
|
253
254
|
- `CONTAINER_APT_PACKAGES`: Space-delimited system packages installed in the generated image
|
|
@@ -263,6 +264,7 @@ addopts = "-ra"
|
|
|
263
264
|
- `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.
|
|
264
265
|
- `CONTAINER_DEPS_MOVE_SCRIPT`: Raw executable script to run after `/tmp/deps` is copied into the image
|
|
265
266
|
- `CONTAINER_DEPS_MOVE_SCRIPT_PATH`: Host path to a dependency move script. This takes precedence over `CONTAINER_DEPS_MOVE_SCRIPT`.
|
|
267
|
+
- `UV_INDEX_{name}_USERNAME` and `UV_INDEX_{name}_PASSWORD`: Private Python index credentials consumed by uv during the Docker build. When any of these are set, the task automatically passes them as BuildKit secrets (`--secret id=uv_index_{name}_username,env=...`) and renders matching `--mount=type=secret` directives in the builder stage so uv can authenticate without baking credentials into the image. Multiple indices are supported; replace `{name}` with the uppercase index name (hyphens as underscores). These can be set in `tool.poe.env` for CI/CD or in the local environment for development.
|
|
266
268
|
|
|
267
269
|
#### Release settings
|
|
268
270
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
common_python_tasks/__init__.py,sha256=tqRLgOwmMQHDb1A84XbML9sWfzMymkSPhJjoF6aCHgA,916
|
|
2
|
+
common_python_tasks/__main__.py,sha256=GpgSd_toMbEaWA_cZvgsZE7jo4XpzuI-awBVm2uhVyw,3512
|
|
3
|
+
common_python_tasks/docker.py,sha256=_YW-hyHWlUK-kMCvI6QTgxSfmXQQKhYLOKRBPfUmtHA,24176
|
|
4
|
+
common_python_tasks/docker_compose.py,sha256=tEChLNxlcBMQXy2wjZrVsmFxFrvt3CmJYBpc-7Q4bK0,21281
|
|
5
|
+
common_python_tasks/env.py,sha256=QSSu4NbnV5oKqMBDBAXPh6oJHYmumoAZRRpRmSQsmvk,19662
|
|
6
|
+
common_python_tasks/git.py,sha256=znfLSEwAUYL6nFK4fjLhzdjFu9Pc3ppPdU9m2QCHaKw,11511
|
|
7
|
+
common_python_tasks/github.py,sha256=_LVanl-z4PsdLM94L8y7tkX-AZO2n3Qy39FRY7reDhA,21589
|
|
8
|
+
common_python_tasks/project.py,sha256=pkk5flChUjwBM95yhlsNqMxlL_8oo31b-G5iaXjoXas,17181
|
|
9
|
+
common_python_tasks/tasks.py,sha256=sTG4GtUXVxQ8ZWKWGuWdghI03DFyXLbSee8Xc6g0nv0,59519
|
|
10
|
+
common_python_tasks/utils.py,sha256=hR7w0Cb7uQ7ILyIegTQ4znWHVoWANlCgxrOnO-Rh5oE,20798
|
|
11
|
+
common_python_tasks/data/dockerfile_extensions/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
common_python_tasks/data/fastapi/alembic.ini.j2,sha256=dOiAmZngzX6eyVXeM6BhzgQmes2zwUEdKwOgm8mxjMs,985
|
|
13
|
+
common_python_tasks/data/fastapi/compose-base.yml.j2,sha256=vDLPAdRJ66lsWZdsKbYqEvY_liXaHZ0ly4ljzqmJsyY,910
|
|
14
|
+
common_python_tasks/data/fastapi/compose-db-debug.yml.j2,sha256=iREdSy4F8tJ5m9rlB6CROBkj9OtDFVuGphqIuDvHhww,1193
|
|
15
|
+
common_python_tasks/data/fastapi/compose-db.yml.j2,sha256=16i2rrsYPIJIQn4gpopr5-8KBirMS7OwpXHPaAC03k8,1777
|
|
16
|
+
common_python_tasks/data/fastapi/compose-debug.yml.j2,sha256=sIH9IFvmRAszN6wSejpGVQygXh1g2lVmlzOgPhTx5CY,661
|
|
17
|
+
common_python_tasks/data/generic/.coveragerc,sha256=8zXwDq18wIKQV4NtW2NJYpo3nxCO4LsAChyluYTUoJc,85
|
|
18
|
+
common_python_tasks/data/generic/.dockerignore,sha256=8kHwNa6Ct-6y3EvRjNuhOKfquuGEywq3P2BHIbG4uXo,53
|
|
19
|
+
common_python_tasks/data/generic/Dockerfile.deps.j2,sha256=fBmcytyUd4LsC26y0IgZ09QPxrFp0gx0d3OlKNqZ-KM,663
|
|
20
|
+
common_python_tasks/data/generic/Dockerfile.j2,sha256=0K_gNC35wiP1-yOO2UB2WoeCoz0iW5pS7walajsXFPE,6016
|
|
21
|
+
common_python_tasks/data/generic/cliff.toml,sha256=po1MnEGEEgrpyBz3XLWr0h7emXb6ASX63lrU2r6o240,77
|
|
22
|
+
common_python_tasks/data/generic/pytest.ini,sha256=RHak5QrLzKHvUBjFmaaiOYFrd_8YBRQubXEOT0NJfco,134
|
|
23
|
+
common_python_tasks/data/generic/ruff.toml,sha256=njvd-5inSGmgY865SWCT4SR9IrKlzS7JQFZmAG2UT_E,53
|
|
24
|
+
common_python_tasks-0.10.1.dist-info/METADATA,sha256=fN5bMujd9lclICBzrAtsCj9qQD6beileWRLrOXfvfak,22028
|
|
25
|
+
common_python_tasks-0.10.1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
26
|
+
common_python_tasks-0.10.1.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
|
|
27
|
+
common_python_tasks-0.10.1.dist-info/RECORD,,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
common_python_tasks/__init__.py,sha256=HIumMQ0gSD2sVp6efGAILcQJVIUGiasNEij2Nrt3H7k,847
|
|
2
|
-
common_python_tasks/__main__.py,sha256=GpgSd_toMbEaWA_cZvgsZE7jo4XpzuI-awBVm2uhVyw,3512
|
|
3
|
-
common_python_tasks/docker.py,sha256=Xw3VWXkSImccHHgk2krzA1XYCjojvwLsYpXY5wKoYco,24187
|
|
4
|
-
common_python_tasks/docker_compose.py,sha256=OblQjlVa_8HoWNYqu3599gJc34eAYiSgJhrbWuWBJlc,21193
|
|
5
|
-
common_python_tasks/env.py,sha256=BQeRrFBeXMFUK_4Z7cUKprJdXBZn688EHfWrhbm0WD4,16819
|
|
6
|
-
common_python_tasks/git.py,sha256=XdDkrw26B4oJZjNoKMWURFlGwKK3_uBiujXkF6rmVM0,11509
|
|
7
|
-
common_python_tasks/github.py,sha256=_LVanl-z4PsdLM94L8y7tkX-AZO2n3Qy39FRY7reDhA,21589
|
|
8
|
-
common_python_tasks/project.py,sha256=Kz2oZjHPqwRKzIkV4KuOmBzB2wUapaVrUZznQL4FN9c,17128
|
|
9
|
-
common_python_tasks/tasks.py,sha256=HIKDaSC8xR4xi35cGxDm_CmAqjjulh0VQtoiIwUNJ3U,58840
|
|
10
|
-
common_python_tasks/utils.py,sha256=cmplbay7ElSD41qqQrTT5112CtuUkW-AN8o3iTLulBM,20653
|
|
11
|
-
common_python_tasks/data/dockerfile_extensions/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
common_python_tasks/data/fastapi/alembic.ini.j2,sha256=dOiAmZngzX6eyVXeM6BhzgQmes2zwUEdKwOgm8mxjMs,985
|
|
13
|
-
common_python_tasks/data/fastapi/compose-base.yml.j2,sha256=vDLPAdRJ66lsWZdsKbYqEvY_liXaHZ0ly4ljzqmJsyY,910
|
|
14
|
-
common_python_tasks/data/fastapi/compose-db-debug.yml.j2,sha256=iREdSy4F8tJ5m9rlB6CROBkj9OtDFVuGphqIuDvHhww,1193
|
|
15
|
-
common_python_tasks/data/fastapi/compose-db.yml.j2,sha256=16i2rrsYPIJIQn4gpopr5-8KBirMS7OwpXHPaAC03k8,1777
|
|
16
|
-
common_python_tasks/data/fastapi/compose-debug.yml.j2,sha256=sIH9IFvmRAszN6wSejpGVQygXh1g2lVmlzOgPhTx5CY,661
|
|
17
|
-
common_python_tasks/data/generic/.coveragerc,sha256=8zXwDq18wIKQV4NtW2NJYpo3nxCO4LsAChyluYTUoJc,85
|
|
18
|
-
common_python_tasks/data/generic/.dockerignore,sha256=NFTre70gSZ6yi3qg5jxyvcnj3G7FazXLgi--6fOJfQk,65
|
|
19
|
-
common_python_tasks/data/generic/Dockerfile.deps.j2,sha256=rCGsBwLHQIRsGFzDbiRi_nWc-SZ3MAFZ_BeZJFV28Ck,432
|
|
20
|
-
common_python_tasks/data/generic/Dockerfile.j2,sha256=l5PRPWg3R4mkLMb_WEQ_f8pLLSYLH-Y2fWIgy5wSCvQ,5620
|
|
21
|
-
common_python_tasks/data/generic/cliff.toml,sha256=po1MnEGEEgrpyBz3XLWr0h7emXb6ASX63lrU2r6o240,77
|
|
22
|
-
common_python_tasks/data/generic/pytest.ini,sha256=RHak5QrLzKHvUBjFmaaiOYFrd_8YBRQubXEOT0NJfco,134
|
|
23
|
-
common_python_tasks/data/generic/ruff.toml,sha256=njvd-5inSGmgY865SWCT4SR9IrKlzS7JQFZmAG2UT_E,53
|
|
24
|
-
common_python_tasks-0.9.0.dist-info/METADATA,sha256=XpzIcJPOkek42mCbOnlPJb1Iw5IqLQAsYaQk2LaHe40,21049
|
|
25
|
-
common_python_tasks-0.9.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
26
|
-
common_python_tasks-0.9.0.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
|
|
27
|
-
common_python_tasks-0.9.0.dist-info/RECORD,,
|
|
File without changes
|
{common_python_tasks-0.9.0.dist-info → common_python_tasks-0.10.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|