common-python-tasks 0.4.0__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/Dockerfile.j2 +40 -7
- common_python_tasks/docker.py +30 -18
- common_python_tasks/docker_compose.py +7 -4
- common_python_tasks/git.py +3 -6
- common_python_tasks/project.py +287 -26
- common_python_tasks/tasks.py +13 -30
- common_python_tasks/utils.py +24 -0
- {common_python_tasks-0.4.0.dist-info → common_python_tasks-0.4.1.dist-info}/METADATA +55 -38
- {common_python_tasks-0.4.0.dist-info → common_python_tasks-0.4.1.dist-info}/RECORD +15 -15
- {common_python_tasks-0.4.0.dist-info → common_python_tasks-0.4.1.dist-info}/WHEEL +1 -1
- {common_python_tasks-0.4.0.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,24 +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 PACKAGE_VERSION
|
|
17
20
|
|
|
18
21
|
# Bypass VCS-based version detection: use the host-computed version directly.
|
|
19
22
|
# This avoids requiring a .git directory in the build context and prevents
|
|
20
23
|
# false dirty-version detection from a partial working tree.
|
|
21
24
|
ENV POETRY_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION}
|
|
22
25
|
|
|
23
|
-
# Install Poetry and required Poetry plugins for build
|
|
24
|
-
RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }}
|
|
25
|
-
|
|
26
|
-
"poetry
|
|
27
|
-
"poetry
|
|
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 %}
|
|
28
52
|
|
|
29
53
|
# Build package
|
|
30
54
|
WORKDIR /tmp/build
|
|
@@ -32,15 +56,24 @@ COPY . /tmp/build/
|
|
|
32
56
|
|
|
33
57
|
{% if HAS_DEBUG_DEPS %}
|
|
34
58
|
# Export debug requirements when the project defines a debug dependency group
|
|
59
|
+
{% if package_manager == 'poetry' %}
|
|
35
60
|
RUN --mount=type=cache,target=/root/.cache/pip,id=pip-cache{{ CACHE_ID_SUFFIX }} \
|
|
36
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 %}
|
|
37
66
|
{% endif %}
|
|
38
67
|
|
|
39
|
-
# Build the wheel, caching
|
|
68
|
+
# Build the wheel, caching the package-manager cache directory to speed up subsequent builds.
|
|
40
69
|
# If running this in a CI system, you must be using a persistent builder to take
|
|
41
70
|
# advantage of the cache mount, and you should configure your CI to persist the
|
|
42
71
|
# cache directory between builds
|
|
72
|
+
{% if package_manager == 'poetry' %}
|
|
43
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 %}
|
|
44
77
|
|
|
45
78
|
FROM python:${PYTHON_VERSION}-${PYTHON_VARIANT} AS runtime
|
|
46
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],
|
|
@@ -331,12 +330,12 @@ def build_deps_image(
|
|
|
331
330
|
|
|
332
331
|
full_tag = f"{deps_image_name}:{deps_tag}"
|
|
333
332
|
build_cmd = _build_docker_build_command(
|
|
334
|
-
context_path
|
|
335
|
-
dockerfile_path
|
|
336
|
-
build_args
|
|
337
|
-
archs
|
|
338
|
-
no_cache
|
|
339
|
-
plain
|
|
333
|
+
context_path,
|
|
334
|
+
dockerfile_path,
|
|
335
|
+
build_args,
|
|
336
|
+
archs,
|
|
337
|
+
no_cache,
|
|
338
|
+
plain,
|
|
340
339
|
tags=[full_tag],
|
|
341
340
|
)
|
|
342
341
|
|
|
@@ -451,24 +450,37 @@ def build_image(
|
|
|
451
450
|
commit_tag = version_tag
|
|
452
451
|
|
|
453
452
|
python_version = platform.python_version()
|
|
454
|
-
|
|
453
|
+
package_manager = project.get_package_manager()
|
|
454
|
+
package_manager_version = project.get_package_manager_version(package_manager)
|
|
455
455
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
"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,
|
|
460
460
|
"POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION": project.get_installed_requirement_version(
|
|
461
461
|
"poetry-dynamic-versioning[plugin]"
|
|
462
462
|
),
|
|
463
463
|
"POETRY_PLUGIN_EXPORT_VERSION": project.get_installed_requirement_version(
|
|
464
464
|
"poetry-plugin-export"
|
|
465
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,
|
|
466
477
|
"PACKAGE_VERSION": package_version,
|
|
467
478
|
"PACKAGE_NAME": utils.get_package_name(use_underscores=True),
|
|
468
479
|
"AUTHORS": ",".join(
|
|
469
480
|
[f"{name} <{email}>" for name, email in project.get_authors()]
|
|
470
481
|
),
|
|
471
482
|
"GIT_COMMIT": commit_tag,
|
|
483
|
+
**manager_build_args,
|
|
472
484
|
},
|
|
473
485
|
extra_build_args,
|
|
474
486
|
)
|
|
@@ -503,12 +515,12 @@ def build_image(
|
|
|
503
515
|
all_tags.append(resolved_tag)
|
|
504
516
|
|
|
505
517
|
build_cmd = _build_docker_build_command(
|
|
506
|
-
context_path
|
|
507
|
-
dockerfile_path
|
|
508
|
-
build_args
|
|
509
|
-
archs
|
|
510
|
-
no_cache
|
|
511
|
-
plain
|
|
518
|
+
context_path,
|
|
519
|
+
dockerfile_path,
|
|
520
|
+
build_args,
|
|
521
|
+
archs,
|
|
522
|
+
no_cache,
|
|
523
|
+
plain,
|
|
512
524
|
target=None if omit_target else target,
|
|
513
525
|
tags=all_tags,
|
|
514
526
|
)
|
|
@@ -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", ""),
|
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):
|
common_python_tasks/project.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
3
4
|
import re
|
|
5
|
+
import shutil
|
|
4
6
|
import tomllib
|
|
7
|
+
from enum import StrEnum, auto
|
|
5
8
|
from functools import lru_cache
|
|
6
9
|
from importlib import metadata
|
|
7
10
|
from pathlib import Path
|
|
@@ -14,6 +17,170 @@ from . import utils
|
|
|
14
17
|
|
|
15
18
|
LOGGER = logging.getLogger(__name__)
|
|
16
19
|
|
|
20
|
+
PACKAGE_MANAGER_ENV_VAR = "COMMON_PYTHON_TASKS_PACKAGE_MANAGER"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PackageManager(StrEnum):
|
|
24
|
+
"""Supported package-manager backends for build and publish workflows."""
|
|
25
|
+
|
|
26
|
+
POETRY = auto()
|
|
27
|
+
UV = auto()
|
|
28
|
+
AUTO = auto()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_tool_version_output(output: str, tool_name: str) -> str:
|
|
32
|
+
cleaned_output = output.strip()
|
|
33
|
+
if not cleaned_output:
|
|
34
|
+
utils.fatal(f"Unable to determine {tool_name} version from empty output")
|
|
35
|
+
|
|
36
|
+
match = re.search(r"(\d+\.\d+\.\d+(?:[-+._a-zA-Z0-9]*)?)", cleaned_output)
|
|
37
|
+
if match is not None:
|
|
38
|
+
return match.group(1)
|
|
39
|
+
|
|
40
|
+
tokens = cleaned_output.replace("(", " ").replace(")", " ").split()
|
|
41
|
+
if tokens:
|
|
42
|
+
return tokens[-1]
|
|
43
|
+
|
|
44
|
+
utils.fatal(f"Unable to parse {tool_name} version output: {output!r}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _find_package_manager_override() -> tuple[str, str] | None:
|
|
48
|
+
value = os.getenv(PACKAGE_MANAGER_ENV_VAR)
|
|
49
|
+
if value is not None and value.strip():
|
|
50
|
+
return PACKAGE_MANAGER_ENV_VAR, value.strip().lower()
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _ensure_package_manager_available(package_manager: PackageManager) -> None:
|
|
55
|
+
if shutil.which(package_manager) is None:
|
|
56
|
+
utils.fatal(
|
|
57
|
+
f"Requested package manager '{package_manager}' is not installed or not in PATH"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _detect_declared_package_manager(
|
|
62
|
+
pyproject_data: dict[str, Any],
|
|
63
|
+
) -> PackageManager | None:
|
|
64
|
+
build_backend = (
|
|
65
|
+
pyproject_data.get("build-system", {}).get("build-backend", "") or ""
|
|
66
|
+
)
|
|
67
|
+
if isinstance(build_backend, str) and "poetry" in build_backend.lower():
|
|
68
|
+
return PackageManager.POETRY
|
|
69
|
+
|
|
70
|
+
if pyproject_data.get("tool", {}).get("poetry") is not None:
|
|
71
|
+
return PackageManager.POETRY
|
|
72
|
+
|
|
73
|
+
if pyproject_data.get("tool", {}).get("uv") is not None:
|
|
74
|
+
return PackageManager.UV
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_package_manager_auto() -> tuple[PackageManager, str]:
|
|
80
|
+
has_poetry = shutil.which("poetry") is not None
|
|
81
|
+
has_uv = shutil.which("uv") is not None
|
|
82
|
+
|
|
83
|
+
if has_uv and not has_poetry:
|
|
84
|
+
return PackageManager.UV, "uv found and poetry missing"
|
|
85
|
+
|
|
86
|
+
if has_poetry and not has_uv:
|
|
87
|
+
return PackageManager.POETRY, "poetry found and uv missing"
|
|
88
|
+
|
|
89
|
+
if not has_poetry and not has_uv:
|
|
90
|
+
utils.fatal(
|
|
91
|
+
"No supported package manager found in PATH. Install poetry or uv, "
|
|
92
|
+
"or set one via COMMON_PYTHON_TASKS_PACKAGE_MANAGER."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
pyproject_data = read_pyproject_toml()
|
|
96
|
+
declared_manager = _detect_declared_package_manager(pyproject_data)
|
|
97
|
+
if declared_manager is not None:
|
|
98
|
+
return declared_manager, "project metadata"
|
|
99
|
+
|
|
100
|
+
return PackageManager.POETRY, "default fallback"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@lru_cache
|
|
104
|
+
def get_package_manager() -> PackageManager:
|
|
105
|
+
"""Resolve the package manager backend used for build and publish commands.
|
|
106
|
+
|
|
107
|
+
Resolution order:
|
|
108
|
+
1. Environment override via `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`.
|
|
109
|
+
2. Auto-discovery from installed executables and project metadata.
|
|
110
|
+
|
|
111
|
+
Supported values are `poetry`, `uv`, and `auto`.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
The selected package manager backend.
|
|
115
|
+
"""
|
|
116
|
+
override = _find_package_manager_override()
|
|
117
|
+
if override is None:
|
|
118
|
+
package_manager, reason = _resolve_package_manager_auto()
|
|
119
|
+
LOGGER.info(
|
|
120
|
+
"Using package manager backend: %s (%s)",
|
|
121
|
+
package_manager,
|
|
122
|
+
reason,
|
|
123
|
+
)
|
|
124
|
+
return package_manager
|
|
125
|
+
|
|
126
|
+
env_var, raw_value = override
|
|
127
|
+
if raw_value not in PackageManager:
|
|
128
|
+
utils.fatal(
|
|
129
|
+
f"Invalid {env_var} value {raw_value!r}. " "Use one of: poetry, uv, auto."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if raw_value == PackageManager.AUTO:
|
|
133
|
+
package_manager, reason = _resolve_package_manager_auto()
|
|
134
|
+
LOGGER.info(
|
|
135
|
+
"Using package manager backend: %s (%s via %s=%s)",
|
|
136
|
+
package_manager,
|
|
137
|
+
reason,
|
|
138
|
+
env_var,
|
|
139
|
+
raw_value,
|
|
140
|
+
)
|
|
141
|
+
return package_manager
|
|
142
|
+
|
|
143
|
+
package_manager = PackageManager(raw_value)
|
|
144
|
+
_ensure_package_manager_available(package_manager)
|
|
145
|
+
LOGGER.info(
|
|
146
|
+
"Using package manager backend: %s (from %s)",
|
|
147
|
+
package_manager,
|
|
148
|
+
env_var,
|
|
149
|
+
)
|
|
150
|
+
return package_manager
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_package_manager_build_command(wheel_only: bool = False) -> list[str]:
|
|
154
|
+
"""Return the build command for the selected package manager.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
wheel_only: Whether to build only a wheel artifact.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Command tokens for the package build command.
|
|
161
|
+
"""
|
|
162
|
+
package_manager = PackageManager(get_package_manager())
|
|
163
|
+
if package_manager == PackageManager.UV:
|
|
164
|
+
command = ["uv", "build"]
|
|
165
|
+
if wheel_only:
|
|
166
|
+
command.append("--wheel")
|
|
167
|
+
return command
|
|
168
|
+
|
|
169
|
+
command = ["poetry", "build"]
|
|
170
|
+
if wheel_only:
|
|
171
|
+
command += ["--format", "wheel"]
|
|
172
|
+
return command
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_package_manager_publish_command() -> list[str]:
|
|
176
|
+
"""Return the publish command for the selected package manager.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Command tokens for the package publish command.
|
|
180
|
+
"""
|
|
181
|
+
package_manager = PackageManager(get_package_manager())
|
|
182
|
+
return [package_manager.value, "publish"]
|
|
183
|
+
|
|
17
184
|
|
|
18
185
|
@lru_cache
|
|
19
186
|
def get_authors() -> list[tuple[str, str]]:
|
|
@@ -77,11 +244,31 @@ def get_poetry_version() -> str:
|
|
|
77
244
|
Returns:
|
|
78
245
|
The Poetry version parsed from the output of `poetry --version`.
|
|
79
246
|
"""
|
|
80
|
-
return (
|
|
81
|
-
utils.run_command(["poetry", "--version"], capture_output=True)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
247
|
+
return _parse_tool_version_output(
|
|
248
|
+
utils.run_command(["poetry", "--version"], capture_output=True).stdout,
|
|
249
|
+
tool_name="poetry",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def get_package_manager_version(package_manager: PackageManager | None = None) -> str:
|
|
254
|
+
"""Return the version string for the selected package manager.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
package_manager: Optional explicit package manager backend.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Parsed package-manager version.
|
|
261
|
+
"""
|
|
262
|
+
selected_manager = PackageManager(package_manager or get_package_manager())
|
|
263
|
+
if selected_manager == PackageManager.POETRY:
|
|
264
|
+
return get_poetry_version()
|
|
265
|
+
|
|
266
|
+
return _parse_tool_version_output(
|
|
267
|
+
utils.run_command(
|
|
268
|
+
[selected_manager.value, "--version"], capture_output=True
|
|
269
|
+
).stdout,
|
|
270
|
+
tool_name=selected_manager.value,
|
|
271
|
+
)
|
|
85
272
|
|
|
86
273
|
|
|
87
274
|
@lru_cache
|
|
@@ -94,8 +281,8 @@ def get_installed_requirement_version(requirement_name: str) -> str | None:
|
|
|
94
281
|
|
|
95
282
|
It normalizes extras like `package[extra]` and tries common name variants
|
|
96
283
|
with hyphens and underscores. If the package is not available to
|
|
97
|
-
importlib.metadata, it falls back to
|
|
98
|
-
|
|
284
|
+
importlib.metadata, it falls back to package-manager runtime and package
|
|
285
|
+
listing commands.
|
|
99
286
|
|
|
100
287
|
Args:
|
|
101
288
|
requirement_name: The requirement name to lookup.
|
|
@@ -103,8 +290,6 @@ def get_installed_requirement_version(requirement_name: str) -> str | None:
|
|
|
103
290
|
Returns:
|
|
104
291
|
The installed version string, or `None` if the package cannot be resolved.
|
|
105
292
|
"""
|
|
106
|
-
package_name = requirement_name.split("[", 1)[0]
|
|
107
|
-
|
|
108
293
|
package_name = requirement_name.split("[", 1)[0]
|
|
109
294
|
candidates = {
|
|
110
295
|
package_name,
|
|
@@ -118,10 +303,12 @@ def get_installed_requirement_version(requirement_name: str) -> str | None:
|
|
|
118
303
|
except metadata.PackageNotFoundError:
|
|
119
304
|
pass
|
|
120
305
|
|
|
121
|
-
|
|
306
|
+
package_manager = PackageManager(get_package_manager())
|
|
307
|
+
|
|
308
|
+
def _query_runtime_version(candidate: str) -> str | None:
|
|
122
309
|
result = utils.run_command(
|
|
123
310
|
[
|
|
124
|
-
|
|
311
|
+
package_manager,
|
|
125
312
|
"run",
|
|
126
313
|
"python",
|
|
127
314
|
"-c",
|
|
@@ -139,35 +326,39 @@ def get_installed_requirement_version(requirement_name: str) -> str | None:
|
|
|
139
326
|
return version
|
|
140
327
|
return None
|
|
141
328
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
329
|
+
if package_manager == PackageManager.POETRY:
|
|
330
|
+
version = get_local_poetry_plugin_version(package_name)
|
|
331
|
+
if version:
|
|
332
|
+
return version
|
|
145
333
|
|
|
146
334
|
for candidate in candidates:
|
|
147
|
-
version =
|
|
335
|
+
version = _query_runtime_version(candidate)
|
|
148
336
|
if version:
|
|
149
337
|
return version
|
|
150
338
|
|
|
151
|
-
def
|
|
339
|
+
def _parse_package_show_version(output: str) -> str | None:
|
|
152
340
|
for line in output.splitlines():
|
|
153
341
|
if ":" not in line:
|
|
154
342
|
continue
|
|
155
343
|
key, value = line.split(":", 1)
|
|
156
|
-
if key.strip() == "version":
|
|
344
|
+
if key.strip().lower() == "version":
|
|
157
345
|
return value.strip()
|
|
158
346
|
return None
|
|
159
347
|
|
|
160
|
-
|
|
161
|
-
["poetry", "show", package_name],
|
|
162
|
-
|
|
163
|
-
|
|
348
|
+
show_commands = (
|
|
349
|
+
(["poetry", "show", package_name], ["poetry", "self", "show", package_name])
|
|
350
|
+
if package_manager == PackageManager.POETRY
|
|
351
|
+
else (["uv", "pip", "show", package_name],)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
for cmd in show_commands:
|
|
164
355
|
result = utils.run_command(
|
|
165
|
-
cmd,
|
|
356
|
+
list(cmd),
|
|
166
357
|
capture_output=True,
|
|
167
358
|
acceptable_returncodes={0, 1},
|
|
168
359
|
)
|
|
169
360
|
if result.returncode == 0:
|
|
170
|
-
version =
|
|
361
|
+
version = _parse_package_show_version(result.stdout)
|
|
171
362
|
if version:
|
|
172
363
|
return version
|
|
173
364
|
return None
|
|
@@ -244,7 +435,7 @@ def resolve_container_entrypoint_command(custom_entrypoint: str | None = None) -
|
|
|
244
435
|
|
|
245
436
|
|
|
246
437
|
def _extract_tags_from_script_argument(
|
|
247
|
-
script_value: str,
|
|
438
|
+
script_value: str, script_key: str, argument_key: str
|
|
248
439
|
) -> set[str] | None:
|
|
249
440
|
pattern = re.compile(rf"{re.escape(argument_key)}\s*=\s*(\[[^\]]*\])")
|
|
250
441
|
match = pattern.search(script_value)
|
|
@@ -358,10 +549,80 @@ def get_project_version_from_poetry() -> str:
|
|
|
358
549
|
return parts[1].strip()
|
|
359
550
|
|
|
360
551
|
|
|
552
|
+
def _get_project_version_from_pyproject() -> str | None:
|
|
553
|
+
"""Return `project.version` from pyproject metadata when present.
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
The configured `project.version`, or `None` if missing.
|
|
557
|
+
"""
|
|
558
|
+
version = read_pyproject_toml().get("project", {}).get("version")
|
|
559
|
+
if isinstance(version, str) and version.strip():
|
|
560
|
+
return version.strip()
|
|
561
|
+
return None
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def get_project_version_from_vcs() -> str | None:
|
|
565
|
+
"""Return a PEP 440 version derived from VCS tags.
|
|
566
|
+
|
|
567
|
+
Returns:
|
|
568
|
+
A version string when git metadata is available; otherwise `None`.
|
|
569
|
+
"""
|
|
570
|
+
try:
|
|
571
|
+
from dunamai import Style, Version
|
|
572
|
+
|
|
573
|
+
return Version.from_git().serialize(style=Style.Pep440, dirty=False)
|
|
574
|
+
except Exception:
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def get_project_version() -> str:
|
|
579
|
+
"""Return the current project version for release and tagging workflows.
|
|
580
|
+
|
|
581
|
+
For Poetry projects, this uses `poetry version` to preserve
|
|
582
|
+
poetry-dynamic-versioning behavior. For uv workflows, it prefers
|
|
583
|
+
`project.version` and falls back to VCS-derived versioning.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
The resolved current project version.
|
|
587
|
+
"""
|
|
588
|
+
package_manager = PackageManager(get_package_manager())
|
|
589
|
+
if package_manager == PackageManager.POETRY:
|
|
590
|
+
return get_project_version_from_poetry()
|
|
591
|
+
|
|
592
|
+
pyproject_version = _get_project_version_from_pyproject()
|
|
593
|
+
if pyproject_version and pyproject_version not in {"0.0.0", "0.0.0.dev0"}:
|
|
594
|
+
return pyproject_version
|
|
595
|
+
|
|
596
|
+
vcs_version = get_project_version_from_vcs()
|
|
597
|
+
if vcs_version is not None:
|
|
598
|
+
return vcs_version
|
|
599
|
+
|
|
600
|
+
if pyproject_version:
|
|
601
|
+
LOGGER.warning(
|
|
602
|
+
"Using placeholder project.version=%s because VCS version could not be resolved",
|
|
603
|
+
pyproject_version,
|
|
604
|
+
)
|
|
605
|
+
return pyproject_version
|
|
606
|
+
|
|
607
|
+
utils.fatal("Unable to determine project version from pyproject metadata or VCS")
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def get_release_tag_from_project_version() -> str:
|
|
611
|
+
"""Return release tag string in `v<version>` form.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
Release tag prefixed with `v`.
|
|
615
|
+
"""
|
|
616
|
+
return f"v{get_project_version()}"
|
|
617
|
+
|
|
618
|
+
|
|
361
619
|
def get_release_tag_from_poetry_version() -> str:
|
|
362
|
-
"""Return release tag string in `v<version>` form
|
|
620
|
+
"""Return release tag string in `v<version>` form.
|
|
621
|
+
|
|
622
|
+
This function is retained for backward compatibility and now uses the
|
|
623
|
+
selected package-manager backend.
|
|
363
624
|
|
|
364
625
|
Returns:
|
|
365
626
|
The release tag string prefixed with `v`.
|
|
366
627
|
"""
|
|
367
|
-
return
|
|
628
|
+
return get_release_tag_from_project_version()
|
common_python_tasks/tasks.py
CHANGED
|
@@ -5,6 +5,7 @@ import os
|
|
|
5
5
|
from functools import partial, wraps
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from common_python_tasks.utils import confirm
|
|
8
9
|
from poethepoet_tasks import TaskCollection
|
|
9
10
|
|
|
10
11
|
|
|
@@ -116,7 +117,6 @@ def _wrap_task_function(func: callable, task_name: str | None) -> callable:
|
|
|
116
117
|
|
|
117
118
|
def _script(
|
|
118
119
|
func=None,
|
|
119
|
-
*,
|
|
120
120
|
task_name: str | None = None,
|
|
121
121
|
help: str | None = None,
|
|
122
122
|
task_args: bool = True,
|
|
@@ -158,28 +158,13 @@ def _should_update_release_changelog() -> bool:
|
|
|
158
158
|
return env_truthy("RELEASE_UPDATE_CHANGELOG")
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
def _confirm(message: str) -> bool:
|
|
162
|
-
yes_values = {"y", "yes"}
|
|
163
|
-
no_values = {"n", "no"}
|
|
164
|
-
while True:
|
|
165
|
-
try:
|
|
166
|
-
response = input(f"{message} [y/N]: ").strip().lower()
|
|
167
|
-
except (EOFError, OSError):
|
|
168
|
-
return False
|
|
169
|
-
|
|
170
|
-
if response in yes_values:
|
|
171
|
-
return True
|
|
172
|
-
if response in no_values:
|
|
173
|
-
return False
|
|
174
|
-
|
|
175
|
-
|
|
176
161
|
def _confirm_release_with_uncommitted_changes(dirty_files: list[Path]) -> bool:
|
|
177
162
|
preview = ", ".join(str(path) for path in dirty_files[:5])
|
|
178
163
|
if len(dirty_files) > 5:
|
|
179
164
|
preview = f"{preview}, ... and {len(dirty_files) - 5} more"
|
|
180
165
|
|
|
181
166
|
LOGGER.warning("Repository has uncommitted changes: %s", preview)
|
|
182
|
-
return
|
|
167
|
+
return confirm("Proceed with release anyway?")
|
|
183
168
|
|
|
184
169
|
|
|
185
170
|
def _resolve_allow_dirty_release(dirty_files: list[Path]) -> bool:
|
|
@@ -560,6 +545,7 @@ def build_image(
|
|
|
560
545
|
resolve_extension_content,
|
|
561
546
|
)
|
|
562
547
|
from .project import (
|
|
548
|
+
get_package_manager,
|
|
563
549
|
has_debug_dependency_group,
|
|
564
550
|
resolve_container_entrypoint_command,
|
|
565
551
|
)
|
|
@@ -695,6 +681,7 @@ def build_image(
|
|
|
695
681
|
dockerfile_text = render_template_text(
|
|
696
682
|
load_data_file("Dockerfile.j2")[1],
|
|
697
683
|
{
|
|
684
|
+
"PACKAGE_MANAGER": get_package_manager(),
|
|
698
685
|
"EXTENSION_CONTENT": combined_content,
|
|
699
686
|
"DEPS_IMAGE": deps_image_tag,
|
|
700
687
|
"CONTAINER_DEPS_MOVE_SCRIPT": container_deps_move_script,
|
|
@@ -783,7 +770,6 @@ def build_deps_image_task(
|
|
|
783
770
|
@tasks.script(tags=["containers"])
|
|
784
771
|
def run_container(
|
|
785
772
|
tag: str | None = None,
|
|
786
|
-
*,
|
|
787
773
|
entrypoint: str | None = None,
|
|
788
774
|
command: str | None = None,
|
|
789
775
|
root: bool = False,
|
|
@@ -976,11 +962,12 @@ def publish_package(build_first: bool = True) -> None:
|
|
|
976
962
|
Args:
|
|
977
963
|
build_first: If `True`, build the package before publishing.
|
|
978
964
|
"""
|
|
965
|
+
from .project import get_package_manager_publish_command
|
|
979
966
|
from .utils import run_command
|
|
980
967
|
|
|
981
968
|
if build_first:
|
|
982
969
|
build_package(clean_dist=True)
|
|
983
|
-
run_command(
|
|
970
|
+
run_command(get_package_manager_publish_command())
|
|
984
971
|
|
|
985
972
|
|
|
986
973
|
@tasks.script(
|
|
@@ -988,7 +975,6 @@ def publish_package(build_first: bool = True) -> None:
|
|
|
988
975
|
)
|
|
989
976
|
def publish_github_release(
|
|
990
977
|
tag_name: str | None = None,
|
|
991
|
-
*,
|
|
992
978
|
release_name: str | None = None,
|
|
993
979
|
body: str | None = None,
|
|
994
980
|
prerelease: bool = False,
|
|
@@ -1001,12 +987,12 @@ def publish_github_release(
|
|
|
1001
987
|
get_github_release_asset_paths,
|
|
1002
988
|
)
|
|
1003
989
|
from .github import publish_github_release as publish_github_release_helper
|
|
1004
|
-
from .project import
|
|
990
|
+
from .project import get_release_tag_from_project_version
|
|
1005
991
|
from .utils import fatal
|
|
1006
992
|
|
|
1007
993
|
resolved_tag_name = tag_name or os.getenv("GITHUB_RELEASE_TAG")
|
|
1008
994
|
if resolved_tag_name is None:
|
|
1009
|
-
resolved_tag_name =
|
|
995
|
+
resolved_tag_name = get_release_tag_from_project_version()
|
|
1010
996
|
if not resolved_tag_name:
|
|
1011
997
|
fatal("Unable to determine the release tag for GitHub Release publication")
|
|
1012
998
|
|
|
@@ -1029,11 +1015,10 @@ def publish_github_release(
|
|
|
1029
1015
|
@tasks.script(task_name="build-package", tags=["packaging", "build", "common"])
|
|
1030
1016
|
def build_package(wheel_only: bool = False, clean_dist: bool = False) -> None:
|
|
1031
1017
|
"""Build the package (wheel and sdist)."""
|
|
1018
|
+
from .project import get_package_manager_build_command
|
|
1032
1019
|
from .utils import run_command
|
|
1033
1020
|
|
|
1034
|
-
command =
|
|
1035
|
-
if wheel_only:
|
|
1036
|
-
command += ["--format", "wheel"]
|
|
1021
|
+
command = get_package_manager_build_command(wheel_only=wheel_only)
|
|
1037
1022
|
if clean_dist:
|
|
1038
1023
|
dist_path = Path("dist")
|
|
1039
1024
|
if dist_path.exists() and any(dist_path.iterdir()):
|
|
@@ -1044,7 +1029,6 @@ def build_package(wheel_only: bool = False, clean_dist: bool = False) -> None:
|
|
|
1044
1029
|
@tasks.script(tags=["packaging", "common"])
|
|
1045
1030
|
def bump_version(
|
|
1046
1031
|
component: str = "auto",
|
|
1047
|
-
*,
|
|
1048
1032
|
stage: str | None = None,
|
|
1049
1033
|
dry_run: bool = False,
|
|
1050
1034
|
allow_dirty: bool = False,
|
|
@@ -1106,10 +1090,9 @@ def changelog() -> None:
|
|
|
1106
1090
|
|
|
1107
1091
|
def _run_release_flow(
|
|
1108
1092
|
component: str = "auto",
|
|
1109
|
-
*,
|
|
1110
1093
|
stage: str | None = None,
|
|
1111
1094
|
dry_run: bool = False,
|
|
1112
|
-
include_containers: bool,
|
|
1095
|
+
include_containers: bool = False,
|
|
1113
1096
|
debug: bool = False,
|
|
1114
1097
|
no_cache: bool = False,
|
|
1115
1098
|
plain: bool = False,
|
|
@@ -1224,7 +1207,6 @@ def _run_release_flow(
|
|
|
1224
1207
|
@tasks.script(task_name="release", tags=["packaging", "release", "containers"])
|
|
1225
1208
|
def release(
|
|
1226
1209
|
component: str = "auto",
|
|
1227
|
-
*,
|
|
1228
1210
|
stage: str | None = None,
|
|
1229
1211
|
dry_run: bool = False,
|
|
1230
1212
|
debug: bool = False,
|
|
@@ -1278,7 +1260,6 @@ def release(
|
|
|
1278
1260
|
@tasks.script(task_name="release", tags=["packaging", "release", "common"])
|
|
1279
1261
|
def release_without_containers(
|
|
1280
1262
|
component: str = "auto",
|
|
1281
|
-
*,
|
|
1282
1263
|
stage: str | None = None,
|
|
1283
1264
|
dry_run: bool = False,
|
|
1284
1265
|
assets: list[str] | None = None,
|
|
@@ -1431,6 +1412,7 @@ def fastapi_stack_up(
|
|
|
1431
1412
|
load_container_env_tokens,
|
|
1432
1413
|
)
|
|
1433
1414
|
from .project import (
|
|
1415
|
+
get_package_manager,
|
|
1434
1416
|
has_debug_dependency_group,
|
|
1435
1417
|
resolve_container_entrypoint_command,
|
|
1436
1418
|
)
|
|
@@ -1453,6 +1435,7 @@ def fastapi_stack_up(
|
|
|
1453
1435
|
dockerfile_text = render_template_text(
|
|
1454
1436
|
load_data_file("Dockerfile.j2")[1],
|
|
1455
1437
|
{
|
|
1438
|
+
"PACKAGE_MANAGER": get_package_manager(),
|
|
1456
1439
|
"EXTENSION_CONTENT": "",
|
|
1457
1440
|
"HAS_DEBUG_DEPS": has_debug_deps,
|
|
1458
1441
|
"APT_PACKAGES": "",
|
common_python_tasks/utils.py
CHANGED
|
@@ -585,3 +585,27 @@ def changelog() -> str | None:
|
|
|
585
585
|
result,
|
|
586
586
|
count=1,
|
|
587
587
|
).strip()
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def confirm(message: str) -> bool:
|
|
591
|
+
"""Prompt the user for a yes/no confirmation.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
message: The confirmation message to display.
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
`True` if the user confirms with 'y' or 'yes', `False` for 'n', 'no', or empty
|
|
598
|
+
input. The prompt will repeat until a valid response is received.
|
|
599
|
+
"""
|
|
600
|
+
yes_values = {"y", "yes"}
|
|
601
|
+
no_values = {"n", "no"}
|
|
602
|
+
while True:
|
|
603
|
+
try:
|
|
604
|
+
response = input(f"{message} [y/N]: ").strip().lower()
|
|
605
|
+
except (EOFError, OSError):
|
|
606
|
+
return False
|
|
607
|
+
|
|
608
|
+
if response in yes_values:
|
|
609
|
+
return True
|
|
610
|
+
if response in no_values:
|
|
611
|
+
return False
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: common-python-tasks
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Opinionated Poe the Poet tasks for Python package development.
|
|
5
|
+
Project-URL: Homepage, http://github.com/ci-sourcerer/common-python-tasks
|
|
6
|
+
Project-URL: Source, http://github.com/ci-sourcerer/common-python-tasks.git
|
|
7
|
+
Project-URL: Issues, http://github.com/ci-sourcerer/common-python-tasks/issues
|
|
8
|
+
Author-email: Joseph Asbury <ci_sourcerer@yahoo.com>
|
|
5
9
|
License-Expression: MIT
|
|
6
10
|
License-File: LICENSE
|
|
7
|
-
Author: Joseph Asbury
|
|
8
|
-
Author-email: ci_sourcerer@yahoo.com
|
|
9
|
-
Requires-Python: >=3.11,<4.0
|
|
10
11
|
Classifier: Development Status :: 3 - Alpha
|
|
11
12
|
Classifier: Intended Audience :: Developers
|
|
12
13
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -15,23 +16,21 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
18
|
Classifier: Topic :: Software Development :: Build Tools
|
|
18
|
-
Requires-
|
|
19
|
-
Requires-Dist:
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist:
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist:
|
|
28
|
-
Requires-Dist:
|
|
29
|
-
Requires-Dist: pytest-cov
|
|
30
|
-
Requires-Dist:
|
|
31
|
-
Requires-Dist:
|
|
32
|
-
|
|
33
|
-
Project-URL: Issues, http://github.com/ci-sourcerer/common-python-tasks/issues
|
|
34
|
-
Project-URL: Source, http://github.com/ci-sourcerer/common-python-tasks.git
|
|
19
|
+
Requires-Python: <4.0,>=3.11
|
|
20
|
+
Requires-Dist: autoflake<3.0.0,>=2.3.1
|
|
21
|
+
Requires-Dist: black<27.0.0,>=26.3.1
|
|
22
|
+
Requires-Dist: docker<8.0.0,>=7.1.0
|
|
23
|
+
Requires-Dist: dunamai<2.0.0,>=1.25.0
|
|
24
|
+
Requires-Dist: flake8<8.0.0,>=7.3.0
|
|
25
|
+
Requires-Dist: git-cliff<3.0.0,>=2.12.0
|
|
26
|
+
Requires-Dist: isort<9.0.0,>=8.0.1
|
|
27
|
+
Requires-Dist: jinja2<4.0.0,>=3.1.6
|
|
28
|
+
Requires-Dist: packaging<27.0,>=26.2
|
|
29
|
+
Requires-Dist: poethepoet-tasks<0.4.0,>=0.3.0
|
|
30
|
+
Requires-Dist: pytest-cov<8.0.0,>=7.0.0
|
|
31
|
+
Requires-Dist: pytest<10.0.0,>=9.0.3
|
|
32
|
+
Requires-Dist: requests<3.0.0,>=2.34.2
|
|
33
|
+
Requires-Dist: tomlkit<0.14.0,>=0.13.3
|
|
35
34
|
Description-Content-Type: text/markdown
|
|
36
35
|
|
|
37
36
|
# Common Python Tasks
|
|
@@ -53,7 +52,7 @@ curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents
|
|
|
53
52
|
To install a specific release, set the environment variable `COMMON_PYTHON_TASKS_VERSION`.
|
|
54
53
|
|
|
55
54
|
```shell
|
|
56
|
-
COMMON_PYTHON_TASKS_VERSION=0.4.
|
|
55
|
+
COMMON_PYTHON_TASKS_VERSION=0.4.1 \
|
|
57
56
|
sh -c "$(curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/add-common-python-tasks.sh | jq -r '.content' | base64 -d)"
|
|
58
57
|
```
|
|
59
58
|
|
|
@@ -61,7 +60,7 @@ This will complete the following steps.
|
|
|
61
60
|
|
|
62
61
|
1. Add the latest version of `common-python-tasks` to your `pyproject.toml` dependencies
|
|
63
62
|
2. Configure Poe the Poet to expose the default common task set
|
|
64
|
-
3. Install
|
|
63
|
+
3. Install dependencies for your project
|
|
65
64
|
|
|
66
65
|
**Always review scripts before running them!** Even though I believe I write good software, it's best practice to verify any script you download from the Internet.
|
|
67
66
|
|
|
@@ -74,7 +73,7 @@ There's no real reason to run the automated script; I just like automating every
|
|
|
74
73
|
```toml
|
|
75
74
|
[project]
|
|
76
75
|
dependencies = [
|
|
77
|
-
"common-python-tasks==0.4.
|
|
76
|
+
"common-python-tasks==0.4.1" # Always pin to a specific version
|
|
78
77
|
]
|
|
79
78
|
|
|
80
79
|
[tool.poe]
|
|
@@ -84,6 +83,8 @@ There's no real reason to run the automated script; I just like automating every
|
|
|
84
83
|
2. Install the package
|
|
85
84
|
|
|
86
85
|
```shell
|
|
86
|
+
uv sync
|
|
87
|
+
# or, for Poetry workflows
|
|
87
88
|
poetry install
|
|
88
89
|
```
|
|
89
90
|
|
|
@@ -196,9 +197,24 @@ The `fastapi` stack includes a service for your FastAPI application. It uses the
|
|
|
196
197
|
|
|
197
198
|
Your project must meet the following requirements.
|
|
198
199
|
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
200
|
+
- Uses Poetry or uv for dependency management
|
|
201
|
+
- Uses Poe the Poet for task management
|
|
202
|
+
- Has `poetry-dynamic-versioning` or `uv-dynamic-versioning` as a dependency
|
|
203
|
+
- Has a `pyproject.toml` file at the root
|
|
204
|
+
- Has a package name (automatically inferred from `project.name` in `pyproject.toml` or set via `PACKAGE_NAME` environment variable)
|
|
205
|
+
|
|
206
|
+
### Package manager backend selection
|
|
207
|
+
|
|
208
|
+
Packaging-related tasks (`build-package`, `publish-package`, release tagging/version resolution, and related container build metadata) support both Poetry and uv.
|
|
209
|
+
|
|
210
|
+
Backend selection uses this precedence.
|
|
211
|
+
|
|
212
|
+
1. `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`
|
|
213
|
+
2. `COMMON_PYTHON_TASKS_BUILD_TOOL`
|
|
214
|
+
3. `PACKAGE_MANAGER`
|
|
215
|
+
4. Auto-discovery (`auto`) based on installed tools and project metadata
|
|
216
|
+
|
|
217
|
+
Supported values are `poetry`, `uv`, and `auto`.
|
|
202
218
|
|
|
203
219
|
### Configuration precedence
|
|
204
220
|
|
|
@@ -227,7 +243,9 @@ The following environment variables configure the paths to configuration files.
|
|
|
227
243
|
The following environment variables configure package and container behavior.
|
|
228
244
|
|
|
229
245
|
- `PACKAGE_NAME`: Overrides the package name; the default comes from `pyproject.toml`
|
|
230
|
-
- `
|
|
246
|
+
- `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`: Selects package manager backend (`poetry`, `uv`, or `auto`)
|
|
247
|
+
- `COMMON_PYTHON_TASKS_BUILD_TOOL`: Alias for `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`
|
|
248
|
+
- `PACKAGE_MANAGER`: Alias for `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`
|
|
231
249
|
- `CONTAINER_REGISTRY_USERNAME`: Specifies the container registry username for image tagging; the default is the current local user
|
|
232
250
|
- `CONTAINER_REGISTRY_URL`: Specifies the registry URL (default: `docker.io/{username}`)
|
|
233
251
|
- `CONTAINER_BUILD_ARGS`: Provides additional Docker build arguments in `KEY=VALUE:OTHER=VALUE` format
|
|
@@ -289,8 +307,8 @@ By default, `tasks()` exposes the common task set. You can still include or excl
|
|
|
289
307
|
```toml
|
|
290
308
|
[project]
|
|
291
309
|
name = "simple-cli-tool"
|
|
292
|
-
version = "0.4.
|
|
293
|
-
dependencies = ["common-python-tasks==0.4.
|
|
310
|
+
version = "0.4.1"
|
|
311
|
+
dependencies = ["common-python-tasks==0.4.1"]
|
|
294
312
|
|
|
295
313
|
[tool.poe]
|
|
296
314
|
include_script = "common_python_tasks:tasks()"
|
|
@@ -302,7 +320,7 @@ Available tasks: common defaults such as `format`, `lint`, `test`, and `build`.
|
|
|
302
320
|
|
|
303
321
|
```toml
|
|
304
322
|
[project]
|
|
305
|
-
dependencies = ["common-python-tasks==0.4.
|
|
323
|
+
dependencies = ["common-python-tasks==0.4.1"]
|
|
306
324
|
|
|
307
325
|
[tool.poe]
|
|
308
326
|
include_script = "common_python_tasks:tasks(include_tags=['common', 'containers'])"
|
|
@@ -318,7 +336,7 @@ Available tasks: All tasks including `build-image` and `push-image`.
|
|
|
318
336
|
|
|
319
337
|
```toml
|
|
320
338
|
[project]
|
|
321
|
-
dependencies = ["common-python-tasks==0.4.
|
|
339
|
+
dependencies = ["common-python-tasks==0.4.1"]
|
|
322
340
|
|
|
323
341
|
[tool.poe]
|
|
324
342
|
include_script = "common_python_tasks:tasks()"
|
|
@@ -367,7 +385,7 @@ COMMON_PYTHON_TASKS_LOG_LEVEL=DEBUG poe test
|
|
|
367
385
|
Make sure your `pyproject.toml` contains the following.
|
|
368
386
|
|
|
369
387
|
- A correct package name in `[project]`
|
|
370
|
-
-
|
|
388
|
+
- Package discovery settings required by your chosen build backend (for Poetry projects, this is commonly `[tool.poetry] packages = [{ include = "your_package", from = "src" }]`)
|
|
371
389
|
|
|
372
390
|
### Stack fails to start or services won't connect
|
|
373
391
|
|
|
@@ -401,9 +419,9 @@ If `SECRET_KEY` or `DB_PASS` aren't auto-generated:
|
|
|
401
419
|
|
|
402
420
|
The standard Python Dockerfile incorporates several intentional design choices.
|
|
403
421
|
|
|
404
|
-
- Multi-stage build: The build stage installs Poetry and builds a wheel while the runtime stage installs only the wheel to keep the final image slim and reproducible
|
|
405
|
-
- Pip and
|
|
406
|
-
- Explicit inputs through build args: `PYTHON_VERSION`, `
|
|
422
|
+
- Multi-stage build: The build stage installs the selected package manager backend (Poetry or uv) and builds a wheel while the runtime stage installs only the wheel to keep the final image slim and reproducible
|
|
423
|
+
- Pip and package-manager cache mounts: Speed up iterative builds without bloating the final image
|
|
424
|
+
- Explicit inputs through build args: `PYTHON_VERSION`, `PACKAGE_MANAGER`, `PACKAGE_MANAGER_VERSION`, `PACKAGE_NAME`, `AUTHORS`, `GIT_COMMIT`, and `CUSTOM_ENTRYPOINT` make image metadata and behavior predictable and auditable
|
|
407
425
|
- Optional debug stage: Exports and installs the `debug` dependency group only when present, without failing otherwise, and is not part of the default final image
|
|
408
426
|
- Stable package path: Creates symlinks to the installed package so entrypoints and consumers have a consistent `/pkg` and `/_$PACKAGE_NAME` path regardless of wheel layout. This ensures that the package can be imported and executed from a known location and supports the less common case of reading files directly from the package path
|
|
409
427
|
- Safe entrypoint selection: The default entrypoint resolves the console script matching the package name, while `CUSTOM_ENTRYPOINT` allows an override at build time and keeps runtime behavior predictable
|
|
@@ -413,7 +431,6 @@ The standard Python Dockerfile incorporates several intentional design choices.
|
|
|
413
431
|
|
|
414
432
|
- This project dogfoods itself; it uses `common-python-tasks` for its own development. **That said, you must set the environment variable `PYTHONPATH=src` when running tasks locally to ensure you are using the local version of the package instead of the installed version**
|
|
415
433
|
- `RELEASE_UPDATE_CHANGELOG` is enabled by default for releases and prepends a tagged section into `CHANGELOG.md` before the release tag is created. Set it to a falsy value if you prefer to manage changelog commits yourself.
|
|
416
|
-
- `RELEASE_PRE_SCRIPT` and `RELEASE_POST_SCRIPT` hooks may not be necessary for most users, as this package promotes
|
|
434
|
+
- `RELEASE_PRE_SCRIPT` and `RELEASE_POST_SCRIPT` hooks may not be necessary for most users, as this package promotes dynamic versioning and `git-cliff` to automate versioning and changelog generation based on git history. One advanced use case for the `RELEASE_PRE_SCRIPT` hook is to edit another file before release, such as a README that references the current stable version number. This allows you to keep the README up to date with the latest version without hardcoding it.
|
|
417
435
|
- Contributions welcome! Open an issue/discussion to discuss changes before submitting a PR. I do not claim to have all the answers, and you can help determine the future of low-code solutions for Python. I am very interested in your feedback as I don't want to work in a vacuum
|
|
418
436
|
- Alpha status: Expect breaking changes between minor versions until 1.0.0
|
|
419
|
-
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
common_python_tasks/__init__.py,sha256=A-qMF818PGbDxM_DH9l1HwQMV-tXn8CZZiVQy-F_sLc,550
|
|
2
2
|
common_python_tasks/__main__.py,sha256=AZiLLusMDs0rZIIBuCDbt6jABlQD-kEwIQIep7GxgEc,3019
|
|
3
|
+
common_python_tasks/docker.py,sha256=KQ_Ym6tungnYqkf1EyXgdLpMjLNX879SWXKM9tIrfEg,19694
|
|
4
|
+
common_python_tasks/docker_compose.py,sha256=UnTP5-Kw31ZzkMUeuml1GxHkHmNsX76-9uezq0qPVBQ,21057
|
|
5
|
+
common_python_tasks/env.py,sha256=DNz-qpFnJ-41pHBb7MIJuemgLqn_rI3GUEZTfC2I4bQ,15064
|
|
6
|
+
common_python_tasks/git.py,sha256=wJ3gePpWE4fhNJYZCEWTDFrhHqaOVkaQveHeiGIbCHo,11188
|
|
7
|
+
common_python_tasks/github.py,sha256=sABDB7BvrdWkpbh4u0-8OeAiu-jWUYfjre_nKOw15bI,18393
|
|
8
|
+
common_python_tasks/project.py,sha256=zyhs1oqptxRALPCzJtNOsQJBeNEbWzKIWBG0BUoiXLg,19271
|
|
9
|
+
common_python_tasks/tasks.py,sha256=P-T6UMhOyhOfOTG2M9EGiK4YfTm9l5mGplF6SnLt1vo,53926
|
|
10
|
+
common_python_tasks/utils.py,sha256=ej4nKFPMFhu1wSgkL9x50DML_sPoPmR6aGcIMyAmfN8,19586
|
|
3
11
|
common_python_tasks/data/dockerfile_extensions/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
12
|
common_python_tasks/data/fastapi/alembic.ini.j2,sha256=IVXmnF1pJvW3o8dXBLIVhmng7DyXu_sWXx_W3VlAcGE,910
|
|
5
|
-
common_python_tasks/data/fastapi/compose-base.yml.j2,sha256=
|
|
13
|
+
common_python_tasks/data/fastapi/compose-base.yml.j2,sha256=JtXilJ0RfdW5PXSF2xDFRxE9-gUcanJBgHWvKdGJWEE,974
|
|
6
14
|
common_python_tasks/data/fastapi/compose-db-debug.yml.j2,sha256=iREdSy4F8tJ5m9rlB6CROBkj9OtDFVuGphqIuDvHhww,1193
|
|
7
|
-
common_python_tasks/data/fastapi/compose-db.yml.j2,sha256=
|
|
15
|
+
common_python_tasks/data/fastapi/compose-db.yml.j2,sha256=FEWgBlebzkgdaaf98xAzDLqk_VGgsYNRF82onqRUhJw,1841
|
|
8
16
|
common_python_tasks/data/fastapi/compose-debug.yml.j2,sha256=6397JFObue-YrweFDPyFg4anwe7H6XaMElRqyBee2Q0,665
|
|
9
17
|
common_python_tasks/data/generic/.coveragerc,sha256=8zXwDq18wIKQV4NtW2NJYpo3nxCO4LsAChyluYTUoJc,85
|
|
10
18
|
common_python_tasks/data/generic/.dockerignore,sha256=K1hgsu8R2Myq6vPIgL86Cvw9aKxly9HfVbcTr3IdKyY,69
|
|
11
19
|
common_python_tasks/data/generic/.flake8,sha256=PzH98ak-AKcENaBCzOhI2eHvLxrneklm7KaIWY9g4so,138
|
|
12
20
|
common_python_tasks/data/generic/.isort.cfg,sha256=P_hdco_n95VeXJjlUcXkChqNdct0SNDuk_n9ZaHzXPE,71
|
|
13
21
|
common_python_tasks/data/generic/Dockerfile.deps.j2,sha256=rCGsBwLHQIRsGFzDbiRi_nWc-SZ3MAFZ_BeZJFV28Ck,432
|
|
14
|
-
common_python_tasks/data/generic/Dockerfile.j2,sha256=
|
|
22
|
+
common_python_tasks/data/generic/Dockerfile.j2,sha256=UG-QxIuFT27j925LsghPl5Wqcw5wNsPPuqB6rcRngz0,7263
|
|
15
23
|
common_python_tasks/data/generic/cliff.toml,sha256=po1MnEGEEgrpyBz3XLWr0h7emXb6ASX63lrU2r6o240,77
|
|
16
24
|
common_python_tasks/data/generic/pytest.ini,sha256=RHak5QrLzKHvUBjFmaaiOYFrd_8YBRQubXEOT0NJfco,134
|
|
17
|
-
common_python_tasks/
|
|
18
|
-
common_python_tasks/
|
|
19
|
-
common_python_tasks/
|
|
20
|
-
common_python_tasks
|
|
21
|
-
common_python_tasks/github.py,sha256=sABDB7BvrdWkpbh4u0-8OeAiu-jWUYfjre_nKOw15bI,18393
|
|
22
|
-
common_python_tasks/project.py,sha256=D0aGytyJy8UPoVXLnOvQB9I3Ty1fhbN_YcVSsvduU64,11053
|
|
23
|
-
common_python_tasks/tasks.py,sha256=HQaEII_lZ4BSnyBgYNRclCphWISh5pO4YlSH-wBAbp8,54013
|
|
24
|
-
common_python_tasks/utils.py,sha256=gnucqE-fbvXqeUryclWaVWH9VTP9cHvmxiXArhrn05I,18917
|
|
25
|
-
common_python_tasks-0.4.0.dist-info/METADATA,sha256=Sy5uHeRjDHx8WI4UbRp8R68WLaAMscCRc5giYRpSmB8,21168
|
|
26
|
-
common_python_tasks-0.4.0.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
|
|
27
|
-
common_python_tasks-0.4.0.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
|
|
28
|
-
common_python_tasks-0.4.0.dist-info/RECORD,,
|
|
25
|
+
common_python_tasks-0.4.1.dist-info/METADATA,sha256=p2tiuAPYuNHa8wx_m9NcgkEbfwQYO9lRY-ox71rOM8g,22054
|
|
26
|
+
common_python_tasks-0.4.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
27
|
+
common_python_tasks-0.4.1.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
|
|
28
|
+
common_python_tasks-0.4.1.dist-info/RECORD,,
|
{common_python_tasks-0.4.0.dist-info → common_python_tasks-0.4.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|