common-python-tasks 0.6.0__py3-none-any.whl → 0.8.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 +9 -0
- common_python_tasks/__main__.py +19 -3
- common_python_tasks/data/fastapi/alembic.ini.j2 +7 -8
- common_python_tasks/data/generic/Dockerfile.j2 +5 -5
- common_python_tasks/data/generic/ruff.toml +2 -0
- common_python_tasks/docker.py +294 -154
- common_python_tasks/docker_compose.py +12 -4
- common_python_tasks/env.py +82 -36
- common_python_tasks/git.py +31 -17
- common_python_tasks/github.py +18 -3
- common_python_tasks/project.py +18 -16
- common_python_tasks/tasks.py +221 -359
- common_python_tasks/utils.py +58 -22
- common_python_tasks-0.8.0.dist-info/METADATA +432 -0
- common_python_tasks-0.8.0.dist-info/RECORD +27 -0
- {common_python_tasks-0.6.0.dist-info → common_python_tasks-0.8.0.dist-info}/WHEEL +1 -1
- common_python_tasks/data/generic/.flake8 +0 -9
- common_python_tasks/data/generic/.isort.cfg +0 -4
- common_python_tasks-0.6.0.dist-info/METADATA +0 -439
- common_python_tasks-0.6.0.dist-info/RECORD +0 -28
- {common_python_tasks-0.6.0.dist-info → common_python_tasks-0.8.0.dist-info}/licenses/LICENSE +0 -0
common_python_tasks/__init__.py
CHANGED
|
@@ -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:
|
common_python_tasks/__main__.py
CHANGED
|
@@ -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:
|
|
@@ -5,16 +5,15 @@ prepend_sys_path = .
|
|
|
5
5
|
output_encoding = utf-8
|
|
6
6
|
|
|
7
7
|
[post_write_hooks]
|
|
8
|
-
hooks =
|
|
8
|
+
hooks = ruff_check,ruff_format
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
ruff_check.type = exec
|
|
11
|
+
ruff_check.executable = ruff
|
|
12
|
+
ruff_check.options = check --fix-only --select F401,I REVISION_SCRIPT_FILENAME
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
isort.type = console_scripts
|
|
17
|
-
isort.entrypoint = isort
|
|
14
|
+
ruff_format.type = exec
|
|
15
|
+
ruff_format.executable = ruff
|
|
16
|
+
ruff_format.options = format REVISION_SCRIPT_FILENAME
|
|
18
17
|
|
|
19
18
|
# Logging configuration
|
|
20
19
|
[loggers]
|
|
@@ -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; \
|
common_python_tasks/docker.py
CHANGED
|
@@ -3,8 +3,10 @@ import logging
|
|
|
3
3
|
import platform
|
|
4
4
|
import re
|
|
5
5
|
import tempfile
|
|
6
|
+
from dataclasses import dataclass
|
|
6
7
|
from enum import IntEnum, StrEnum
|
|
7
8
|
from pathlib import Path
|
|
9
|
+
from shlex import quote
|
|
8
10
|
from typing import Sequence
|
|
9
11
|
|
|
10
12
|
from . import git, project, utils
|
|
@@ -65,10 +67,7 @@ def _merge_build_args(
|
|
|
65
67
|
return build_args
|
|
66
68
|
|
|
67
69
|
|
|
68
|
-
def _create_temp_dockerfile(
|
|
69
|
-
dockerfile_text: str,
|
|
70
|
-
prefix: str,
|
|
71
|
-
) -> tuple[Path, str]:
|
|
70
|
+
def _create_temp_dockerfile(dockerfile_text: str, prefix: str) -> tuple[Path, str]:
|
|
72
71
|
with tempfile.NamedTemporaryFile(
|
|
73
72
|
mode="w",
|
|
74
73
|
encoding="utf-8",
|
|
@@ -101,11 +100,11 @@ def _build_docker_build_command(
|
|
|
101
100
|
plain: bool,
|
|
102
101
|
target: BuildStage | None = None,
|
|
103
102
|
tags: Sequence[str] | None = None,
|
|
103
|
+
docker_build_args: Sequence[str] | None = None,
|
|
104
104
|
) -> list[str]:
|
|
105
105
|
build_cmd = [
|
|
106
106
|
"docker",
|
|
107
107
|
"build",
|
|
108
|
-
str(context_path),
|
|
109
108
|
"-f",
|
|
110
109
|
str(dockerfile_path),
|
|
111
110
|
]
|
|
@@ -124,10 +123,267 @@ def _build_docker_build_command(
|
|
|
124
123
|
build_cmd += ["-t", tag]
|
|
125
124
|
if plain:
|
|
126
125
|
build_cmd += ["--progress", "plain"]
|
|
126
|
+
if docker_build_args:
|
|
127
|
+
build_cmd += [str(arg) for arg in docker_build_args]
|
|
128
|
+
build_cmd.append(str(context_path))
|
|
127
129
|
|
|
128
130
|
return build_cmd
|
|
129
131
|
|
|
130
132
|
|
|
133
|
+
def _run_dockerfile_hook(
|
|
134
|
+
hook_path: Path,
|
|
135
|
+
dockerfile_path: Path,
|
|
136
|
+
context_path: Path,
|
|
137
|
+
debug: bool,
|
|
138
|
+
no_cache: bool,
|
|
139
|
+
plain: bool,
|
|
140
|
+
single_arch: bool,
|
|
141
|
+
) -> None:
|
|
142
|
+
utils.run_command(
|
|
143
|
+
[hook_path, dockerfile_path],
|
|
144
|
+
env={
|
|
145
|
+
"COMMON_PYTHON_TASKS_DOCKERFILE_PATH": str(dockerfile_path),
|
|
146
|
+
"COMMON_PYTHON_TASKS_DOCKER_CONTEXT": str(context_path),
|
|
147
|
+
"COMMON_PYTHON_TASKS_DOCKER_DEBUG": "1" if debug else "0",
|
|
148
|
+
"COMMON_PYTHON_TASKS_DOCKER_NO_CACHE": "1" if no_cache else "0",
|
|
149
|
+
"COMMON_PYTHON_TASKS_DOCKER_PLAIN": "1" if plain else "0",
|
|
150
|
+
"COMMON_PYTHON_TASKS_DOCKER_SINGLE_ARCH": "1" if single_arch else "0",
|
|
151
|
+
},
|
|
152
|
+
always_show_command=True,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass(frozen=True)
|
|
157
|
+
class RenderedBuildPlan:
|
|
158
|
+
"""Canonical render of a Docker image build command and generated artifacts."""
|
|
159
|
+
|
|
160
|
+
dockerfile_text: str
|
|
161
|
+
dockerfile_path: Path
|
|
162
|
+
context_path: Path
|
|
163
|
+
build_args: dict[str, str]
|
|
164
|
+
tags: list[str]
|
|
165
|
+
version_tag: str
|
|
166
|
+
commit_tag: str
|
|
167
|
+
command: list[str]
|
|
168
|
+
command_display: str
|
|
169
|
+
target: BuildStage | None = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def render_build_image(
|
|
173
|
+
dockerfile_path: Path | None = None,
|
|
174
|
+
dockerfile_text: str | None = None,
|
|
175
|
+
context_path: Path | None = None,
|
|
176
|
+
debug: bool = False,
|
|
177
|
+
no_cache: bool = False,
|
|
178
|
+
plain: bool = False,
|
|
179
|
+
single_arch: bool = False,
|
|
180
|
+
omit_target: bool = False,
|
|
181
|
+
image_name: str | None = None,
|
|
182
|
+
extra_build_args: dict[str, str] | None = None,
|
|
183
|
+
docker_build_args: Sequence[str] | None = None,
|
|
184
|
+
dockerfile_hook_path: Path | None = None,
|
|
185
|
+
keep_generated_files: bool = True,
|
|
186
|
+
) -> RenderedBuildPlan:
|
|
187
|
+
"""Render the canonical Docker build command without executing it.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
dockerfile_path: Existing Dockerfile path to build from.
|
|
191
|
+
dockerfile_text: Dockerfile content to render into a temporary file.
|
|
192
|
+
context_path: Build context directory.
|
|
193
|
+
debug: Whether the debug image target should be used.
|
|
194
|
+
no_cache: Whether the Docker build should bypass cache.
|
|
195
|
+
plain: Whether Docker output should be plain text.
|
|
196
|
+
single_arch: Whether to limit the build to the current architecture.
|
|
197
|
+
omit_target: Whether to omit the target stage from the command.
|
|
198
|
+
image_name: Optional image name override.
|
|
199
|
+
extra_build_args: Extra build-time arguments.
|
|
200
|
+
docker_build_args: Additional arguments passed directly to `docker build`.
|
|
201
|
+
dockerfile_hook_path: Optional executable script path that can mutate
|
|
202
|
+
the generated Dockerfile in-place before build.
|
|
203
|
+
keep_generated_files: Whether to leave the generated Dockerfile in place.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The rendered build plan that contains the command and generated artifacts.
|
|
207
|
+
"""
|
|
208
|
+
dist_path = Path("dist")
|
|
209
|
+
if utils.directory_has_contents(dist_path):
|
|
210
|
+
utils.remove_path(dist_path)
|
|
211
|
+
|
|
212
|
+
context_path = _resolve_context_path(context_path)
|
|
213
|
+
|
|
214
|
+
temp_file_path = None
|
|
215
|
+
temp_dockerignore_created = False
|
|
216
|
+
dockerignore_path = context_path / ".dockerignore"
|
|
217
|
+
if dockerfile_path is None:
|
|
218
|
+
if dockerfile_text is None:
|
|
219
|
+
utils.fatal("Either dockerfile_path or dockerfile_text must be provided.")
|
|
220
|
+
dockerfile_path, temp_file_path = _create_temp_dockerfile(
|
|
221
|
+
dockerfile_text,
|
|
222
|
+
prefix="Dockerfile.",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if not dockerignore_path.exists():
|
|
226
|
+
LOGGER.debug("No .dockerignore found; using built-in .dockerignore")
|
|
227
|
+
builtin_dockerignore_content = utils.load_data_file(".dockerignore")[1]
|
|
228
|
+
dockerignore_path.write_text(builtin_dockerignore_content, encoding="utf-8")
|
|
229
|
+
temp_dockerignore_created = True
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
if dockerfile_hook_path is not None:
|
|
233
|
+
if temp_file_path is None:
|
|
234
|
+
dockerfile_path, temp_file_path = _create_temp_dockerfile(
|
|
235
|
+
dockerfile_path.read_text(encoding="utf-8"),
|
|
236
|
+
prefix="Dockerfile.hooked.",
|
|
237
|
+
)
|
|
238
|
+
_run_dockerfile_hook(
|
|
239
|
+
dockerfile_hook_path,
|
|
240
|
+
dockerfile_path,
|
|
241
|
+
context_path,
|
|
242
|
+
debug,
|
|
243
|
+
no_cache,
|
|
244
|
+
plain,
|
|
245
|
+
single_arch,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
archs = _resolve_archs(single_arch)
|
|
249
|
+
files_to_ignore = [Path(".dockerignore")] if temp_dockerignore_created else []
|
|
250
|
+
package_version = git.get_version(ignore=files_to_ignore)
|
|
251
|
+
version_string = git.get_image_tag(ignore=files_to_ignore)
|
|
252
|
+
|
|
253
|
+
dockerfile_source = dockerfile_path.read_text(encoding="utf-8")
|
|
254
|
+
if (
|
|
255
|
+
debug
|
|
256
|
+
and not omit_target
|
|
257
|
+
and not re.search(
|
|
258
|
+
rf"^\s*FROM\s+\S+\s+AS\s+{BuildStage.DEBUG}\s*$",
|
|
259
|
+
dockerfile_source,
|
|
260
|
+
re.IGNORECASE | re.MULTILINE,
|
|
261
|
+
)
|
|
262
|
+
):
|
|
263
|
+
utils.fatal(
|
|
264
|
+
"Debug build requested, but the rendered Dockerfile has no debug stage. "
|
|
265
|
+
"Add a non-empty [dependency-groups].debug in pyproject.toml or build without debug."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if debug:
|
|
269
|
+
suffix = BuildSuffix.DEBUG
|
|
270
|
+
target = BuildStage.DEBUG
|
|
271
|
+
tag = BuildTag.DEBUG
|
|
272
|
+
else:
|
|
273
|
+
suffix = BuildSuffix.NONE
|
|
274
|
+
target = BuildStage.RUNTIME
|
|
275
|
+
tag = BuildTag.LATEST if not git.has_tags_later_in_history() else None
|
|
276
|
+
|
|
277
|
+
version_tag = f"{version_string}{suffix}"
|
|
278
|
+
if git.git_available():
|
|
279
|
+
commit_hash = utils.run_command(
|
|
280
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
281
|
+
capture_output=True,
|
|
282
|
+
acceptable_returncodes={0},
|
|
283
|
+
).stdout.strip()
|
|
284
|
+
dirty_suffix = (
|
|
285
|
+
"-dirty" if git.get_dirty_files(ignore=files_to_ignore) else ""
|
|
286
|
+
)
|
|
287
|
+
commit_tag = f"{commit_hash}{dirty_suffix}{suffix}"
|
|
288
|
+
else:
|
|
289
|
+
commit_tag = version_tag
|
|
290
|
+
|
|
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
|
+
|
|
311
|
+
build_args = _merge_build_args(
|
|
312
|
+
{
|
|
313
|
+
"PYTHON_VERSION": python_version,
|
|
314
|
+
"PACKAGE_MANAGER": package_manager,
|
|
315
|
+
"PACKAGE_MANAGER_VERSION": package_manager_version,
|
|
316
|
+
"PACKAGE_VERSION": package_version,
|
|
317
|
+
"PACKAGE_NAME": utils.get_package_name(use_underscores=True),
|
|
318
|
+
"AUTHORS": ",".join(
|
|
319
|
+
[f"{name} <{email}>" for name, email in project.get_authors()]
|
|
320
|
+
),
|
|
321
|
+
"GIT_COMMIT": commit_tag,
|
|
322
|
+
**manager_build_args,
|
|
323
|
+
},
|
|
324
|
+
extra_build_args,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
build_args.setdefault("WORKDIR_PATH", get_workdir_path())
|
|
328
|
+
|
|
329
|
+
tags_to_use = [t for t in (tag, version_tag, commit_tag) if t is not None]
|
|
330
|
+
LOGGER.debug("Building image with tags: %s", ", ".join(tags_to_use))
|
|
331
|
+
image_short_name = (
|
|
332
|
+
image_name if image_name is not None else utils.get_package_name()
|
|
333
|
+
)
|
|
334
|
+
orig_full_name = utils.get_full_image_name()
|
|
335
|
+
|
|
336
|
+
parsed_full_name = utils.parse_image_reference(orig_full_name)
|
|
337
|
+
short_tags = [
|
|
338
|
+
utils.compose_image_name(image_short_name, tag=t, fully_qualified=False)
|
|
339
|
+
for t in tags_to_use
|
|
340
|
+
]
|
|
341
|
+
full_tags = [
|
|
342
|
+
utils.compose_image_name(
|
|
343
|
+
image_short_name,
|
|
344
|
+
tag=t,
|
|
345
|
+
registry=parsed_full_name["registry"],
|
|
346
|
+
namespace=parsed_full_name["namespace"],
|
|
347
|
+
)
|
|
348
|
+
for t in tags_to_use
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
all_tags = []
|
|
352
|
+
for resolved_tag in [*short_tags, *full_tags]:
|
|
353
|
+
if resolved_tag not in all_tags:
|
|
354
|
+
all_tags.append(resolved_tag)
|
|
355
|
+
|
|
356
|
+
build_cmd = _build_docker_build_command(
|
|
357
|
+
context_path,
|
|
358
|
+
dockerfile_path,
|
|
359
|
+
build_args,
|
|
360
|
+
archs,
|
|
361
|
+
no_cache,
|
|
362
|
+
plain,
|
|
363
|
+
target=None if omit_target else target,
|
|
364
|
+
tags=all_tags,
|
|
365
|
+
docker_build_args=docker_build_args,
|
|
366
|
+
)
|
|
367
|
+
command_display = " ".join(quote(str(arg)) for arg in build_cmd)
|
|
368
|
+
return RenderedBuildPlan(
|
|
369
|
+
dockerfile_text=dockerfile_source,
|
|
370
|
+
dockerfile_path=dockerfile_path,
|
|
371
|
+
context_path=context_path,
|
|
372
|
+
build_args=build_args,
|
|
373
|
+
tags=all_tags,
|
|
374
|
+
version_tag=version_tag,
|
|
375
|
+
commit_tag=commit_tag,
|
|
376
|
+
command=build_cmd,
|
|
377
|
+
command_display=command_display,
|
|
378
|
+
target=None if omit_target else target,
|
|
379
|
+
)
|
|
380
|
+
finally:
|
|
381
|
+
if not keep_generated_files:
|
|
382
|
+
_safe_unlink(temp_file_path)
|
|
383
|
+
if temp_dockerignore_created:
|
|
384
|
+
_safe_unlink(dockerignore_path)
|
|
385
|
+
|
|
386
|
+
|
|
131
387
|
def prune_images_keep(
|
|
132
388
|
full_name: str,
|
|
133
389
|
package_name: str,
|
|
@@ -241,6 +497,7 @@ def build_deps_image(
|
|
|
241
497
|
plain: bool = False,
|
|
242
498
|
single_arch: bool = False,
|
|
243
499
|
extra_build_args: dict[str, str] | None = None,
|
|
500
|
+
docker_build_args: Sequence[str] | None = None,
|
|
244
501
|
cache_id_suffix: str = "",
|
|
245
502
|
) -> str:
|
|
246
503
|
"""Build the dependency collector image and return its full tag.
|
|
@@ -253,6 +510,7 @@ def build_deps_image(
|
|
|
253
510
|
plain: If `True`, use plain output mode for the Docker build.
|
|
254
511
|
single_arch: If `True`, build the image for the current host architecture only.
|
|
255
512
|
extra_build_args: Additional build arguments for the Docker build as a dictionary of `KEY: VALUE` pairs.
|
|
513
|
+
docker_build_args: Additional arguments passed directly to `docker build`.
|
|
256
514
|
cache_id_suffix: A suffix to append to the cache ID.
|
|
257
515
|
|
|
258
516
|
Returns:
|
|
@@ -337,6 +595,7 @@ def build_deps_image(
|
|
|
337
595
|
no_cache,
|
|
338
596
|
plain,
|
|
339
597
|
tags=[full_tag],
|
|
598
|
+
docker_build_args=docker_build_args,
|
|
340
599
|
)
|
|
341
600
|
|
|
342
601
|
LOGGER.info("Building deps image: %s", full_tag)
|
|
@@ -359,6 +618,8 @@ def build_image(
|
|
|
359
618
|
omit_target: bool = False,
|
|
360
619
|
image_name: str | None = None,
|
|
361
620
|
extra_build_args: dict[str, str] | None = None,
|
|
621
|
+
docker_build_args: Sequence[str] | None = None,
|
|
622
|
+
dockerfile_hook_path: Path | None = None,
|
|
362
623
|
) -> tuple[str, str]:
|
|
363
624
|
"""Build the primary image and return its version and commit tags.
|
|
364
625
|
|
|
@@ -373,165 +634,44 @@ def build_image(
|
|
|
373
634
|
omit_target: If `True`, omit the target stage.
|
|
374
635
|
image_name: Name of the image to build.
|
|
375
636
|
extra_build_args: Additional build arguments for the Docker build as a dictionary of `KEY: VALUE` pairs.
|
|
637
|
+
docker_build_args: Additional arguments passed directly to `docker build`.
|
|
638
|
+
dockerfile_hook_path: Optional executable script path that can mutate
|
|
639
|
+
the generated Dockerfile in-place before build.
|
|
376
640
|
|
|
377
641
|
Returns:
|
|
378
642
|
A tuple containing the version tag and commit tag of the built image.
|
|
379
643
|
"""
|
|
380
|
-
dist_path = Path("dist")
|
|
381
|
-
if utils.directory_has_contents(dist_path):
|
|
382
|
-
utils.remove_path(dist_path)
|
|
383
|
-
|
|
384
644
|
context_path = _resolve_context_path(context_path)
|
|
385
|
-
|
|
386
|
-
temp_file_path = None
|
|
387
|
-
if dockerfile_path is None:
|
|
388
|
-
if dockerfile_text is None:
|
|
389
|
-
utils.fatal("Either dockerfile_path or dockerfile_text must be provided.")
|
|
390
|
-
dockerfile_path, temp_file_path = _create_temp_dockerfile(
|
|
391
|
-
dockerfile_text,
|
|
392
|
-
prefix="Dockerfile.",
|
|
393
|
-
)
|
|
394
|
-
|
|
395
645
|
dockerignore_path = context_path / ".dockerignore"
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
LOGGER.debug("No .dockerignore found; using built-in .dockerignore")
|
|
399
|
-
builtin_dockerignore_content = utils.load_data_file(".dockerignore")[1]
|
|
400
|
-
dockerignore_path.write_text(builtin_dockerignore_content, encoding="utf-8")
|
|
401
|
-
temp_dockerignore_created = True
|
|
646
|
+
dockerignore_existed = dockerignore_path.exists()
|
|
647
|
+
temp_dockerfile_created = dockerfile_path is None
|
|
402
648
|
|
|
403
|
-
|
|
649
|
+
plan = render_build_image(
|
|
650
|
+
dockerfile_path=dockerfile_path,
|
|
651
|
+
dockerfile_text=dockerfile_text,
|
|
652
|
+
context_path=context_path,
|
|
653
|
+
debug=debug,
|
|
654
|
+
no_cache=no_cache,
|
|
655
|
+
plain=plain,
|
|
656
|
+
single_arch=single_arch,
|
|
657
|
+
omit_target=omit_target,
|
|
658
|
+
image_name=image_name,
|
|
659
|
+
extra_build_args=extra_build_args,
|
|
660
|
+
docker_build_args=docker_build_args,
|
|
661
|
+
dockerfile_hook_path=dockerfile_hook_path,
|
|
662
|
+
keep_generated_files=True,
|
|
663
|
+
)
|
|
404
664
|
try:
|
|
405
|
-
|
|
406
|
-
files_to_ignore = [Path(".dockerignore")] if temp_dockerignore_created else []
|
|
407
|
-
package_version = git.get_version(ignore=files_to_ignore)
|
|
408
|
-
version_string = git.get_image_tag(ignore=files_to_ignore)
|
|
409
|
-
|
|
410
|
-
dockerfile_source = (
|
|
411
|
-
dockerfile_text
|
|
412
|
-
if dockerfile_text is not None
|
|
413
|
-
else dockerfile_path.read_text(encoding="utf-8")
|
|
414
|
-
)
|
|
415
|
-
if (
|
|
416
|
-
debug
|
|
417
|
-
and not omit_target
|
|
418
|
-
and not re.search(
|
|
419
|
-
rf"^\s*FROM\s+\S+\s+AS\s+{BuildStage.DEBUG}\s*$",
|
|
420
|
-
dockerfile_source,
|
|
421
|
-
re.IGNORECASE | re.MULTILINE,
|
|
422
|
-
)
|
|
423
|
-
):
|
|
424
|
-
utils.fatal(
|
|
425
|
-
"Debug build requested, but the rendered Dockerfile has no debug stage. "
|
|
426
|
-
"Add a non-empty [dependency-groups].debug in pyproject.toml or build without debug."
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
if debug:
|
|
430
|
-
suffix = BuildSuffix.DEBUG
|
|
431
|
-
target = BuildStage.DEBUG
|
|
432
|
-
tag = BuildTag.DEBUG
|
|
433
|
-
else:
|
|
434
|
-
suffix = BuildSuffix.NONE
|
|
435
|
-
target = BuildStage.RUNTIME
|
|
436
|
-
tag = BuildTag.LATEST if not git.has_tags_later_in_history() else None
|
|
437
|
-
|
|
438
|
-
version_tag = f"{version_string}{suffix}"
|
|
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
|
-
|
|
452
|
-
python_version = platform.python_version()
|
|
453
|
-
package_manager = project.get_package_manager()
|
|
454
|
-
package_manager_version = project.get_package_manager_version(package_manager)
|
|
455
|
-
|
|
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,
|
|
460
|
-
"POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION": project.get_installed_requirement_version(
|
|
461
|
-
"poetry-dynamic-versioning[plugin]"
|
|
462
|
-
),
|
|
463
|
-
"POETRY_PLUGIN_EXPORT_VERSION": project.get_installed_requirement_version(
|
|
464
|
-
"poetry-plugin-export"
|
|
465
|
-
),
|
|
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,
|
|
478
|
-
"PACKAGE_NAME": utils.get_package_name(use_underscores=True),
|
|
479
|
-
"AUTHORS": ",".join(
|
|
480
|
-
[f"{name} <{email}>" for name, email in project.get_authors()]
|
|
481
|
-
),
|
|
482
|
-
"GIT_COMMIT": commit_tag,
|
|
483
|
-
**manager_build_args,
|
|
484
|
-
},
|
|
485
|
-
extra_build_args,
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
build_args.setdefault("WORKDIR_PATH", get_workdir_path())
|
|
489
|
-
|
|
490
|
-
tags_to_use = [t for t in (tag, version_tag, commit_tag) if t is not None]
|
|
491
|
-
LOGGER.debug("Building image with tags: %s", ", ".join(tags_to_use))
|
|
492
|
-
image_short_name = (
|
|
493
|
-
image_name if image_name is not None else utils.get_package_name()
|
|
494
|
-
)
|
|
495
|
-
orig_full_name = utils.get_full_image_name()
|
|
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 = []
|
|
513
|
-
for resolved_tag in [*short_tags, *full_tags]:
|
|
514
|
-
if resolved_tag not in all_tags:
|
|
515
|
-
all_tags.append(resolved_tag)
|
|
516
|
-
|
|
517
|
-
build_cmd = _build_docker_build_command(
|
|
518
|
-
context_path,
|
|
519
|
-
dockerfile_path,
|
|
520
|
-
build_args,
|
|
521
|
-
archs,
|
|
522
|
-
no_cache,
|
|
523
|
-
plain,
|
|
524
|
-
target=None if omit_target else target,
|
|
525
|
-
tags=all_tags,
|
|
526
|
-
)
|
|
527
|
-
utils.run_command(build_cmd)
|
|
528
|
-
delete_temp_file = True
|
|
665
|
+
utils.run_command(plan.command)
|
|
529
666
|
finally:
|
|
530
|
-
if
|
|
531
|
-
|
|
532
|
-
|
|
667
|
+
if temp_dockerfile_created or (
|
|
668
|
+
dockerfile_path is not None and plan.dockerfile_path != dockerfile_path
|
|
669
|
+
):
|
|
670
|
+
_safe_unlink(plan.dockerfile_path)
|
|
671
|
+
if not dockerignore_existed:
|
|
533
672
|
_safe_unlink(dockerignore_path)
|
|
534
|
-
|
|
673
|
+
|
|
674
|
+
return plan.version_tag, plan.commit_tag
|
|
535
675
|
|
|
536
676
|
|
|
537
677
|
def build_extension_image(
|
|
@@ -35,6 +35,13 @@ def ensure_alembic_config(compose_type: str) -> tuple[Path | None, bool]:
|
|
|
35
35
|
LOGGER.debug("Using existing alembic.ini")
|
|
36
36
|
return alembic_ini_path, False
|
|
37
37
|
|
|
38
|
+
generated_alembic_ini_path = Path(
|
|
39
|
+
f".{compose_type}.alembic.ini.common-python-tasks"
|
|
40
|
+
)
|
|
41
|
+
if generated_alembic_ini_path.exists():
|
|
42
|
+
LOGGER.debug("Using generated alembic.ini at %s", generated_alembic_ini_path)
|
|
43
|
+
return generated_alembic_ini_path, True
|
|
44
|
+
|
|
38
45
|
result = utils.load_data_file(
|
|
39
46
|
"alembic.ini.j2", type_identifier=compose_type, fatal_on_missing=False
|
|
40
47
|
)
|
|
@@ -45,9 +52,9 @@ def ensure_alembic_config(compose_type: str) -> tuple[Path | None, bool]:
|
|
|
45
52
|
rendered = Template(template_content).render(
|
|
46
53
|
package_name=utils.get_package_name(use_underscores=True),
|
|
47
54
|
)
|
|
48
|
-
|
|
49
|
-
LOGGER.debug("Rendered bundled alembic.ini.j2 to %s",
|
|
50
|
-
return
|
|
55
|
+
generated_alembic_ini_path.write_text(rendered, encoding="utf-8")
|
|
56
|
+
LOGGER.debug("Rendered bundled alembic.ini.j2 to %s", generated_alembic_ini_path)
|
|
57
|
+
return generated_alembic_ini_path, True
|
|
51
58
|
|
|
52
59
|
|
|
53
60
|
def render_file(
|
|
@@ -217,7 +224,7 @@ def get_or_generate_secret(
|
|
|
217
224
|
|
|
218
225
|
|
|
219
226
|
def ensure_secrets_generated() -> None:
|
|
220
|
-
"""Ensure required secrets exist
|
|
227
|
+
"""Ensure required secrets exist and persist them to `.env`."""
|
|
221
228
|
get_or_generate_secret("SECRET_KEY")
|
|
222
229
|
get_or_generate_secret("DB_PASS")
|
|
223
230
|
|
|
@@ -514,6 +521,7 @@ def run_docker_compose_command(
|
|
|
514
521
|
*args: Command arguments (None values are filtered out).
|
|
515
522
|
compose_files: List of compose file paths.
|
|
516
523
|
compose_env: Environment variables for the command.
|
|
524
|
+
tasks: Poe task collection that supplies configured env files.
|
|
517
525
|
"""
|
|
518
526
|
|
|
519
527
|
utils.run_command(
|