common-python-tasks 0.4.1__py3-none-any.whl → 0.5.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/env.py +25 -12
- common_python_tasks/github.py +40 -11
- common_python_tasks/project.py +62 -14
- common_python_tasks/tasks.py +34 -6
- {common_python_tasks-0.4.1.dist-info → common_python_tasks-0.5.1.dist-info}/METADATA +20 -18
- {common_python_tasks-0.4.1.dist-info → common_python_tasks-0.5.1.dist-info}/RECORD +8 -8
- {common_python_tasks-0.4.1.dist-info → common_python_tasks-0.5.1.dist-info}/WHEEL +0 -0
- {common_python_tasks-0.4.1.dist-info → common_python_tasks-0.5.1.dist-info}/licenses/LICENSE +0 -0
common_python_tasks/env.py
CHANGED
|
@@ -186,11 +186,12 @@ def split_delimited_values(
|
|
|
186
186
|
separators: str = ":",
|
|
187
187
|
allow_whitespace: bool = False,
|
|
188
188
|
) -> list[str]:
|
|
189
|
-
"""Split a delimited string while preserving quoted
|
|
189
|
+
"""Split a delimited string while preserving quoted or escaped separators.
|
|
190
190
|
|
|
191
191
|
Args:
|
|
192
192
|
value: The string to split.
|
|
193
|
-
separators: Explicit separator characters to use.
|
|
193
|
+
separators: Explicit separator characters to use. Prefix a separator with
|
|
194
|
+
a backslash to include it literally in a token.
|
|
194
195
|
allow_whitespace: Treat whitespace as additional separators.
|
|
195
196
|
|
|
196
197
|
Returns:
|
|
@@ -207,7 +208,7 @@ def split_delimited_values(
|
|
|
207
208
|
|
|
208
209
|
|
|
209
210
|
def split_colon_delimited_values(value: str) -> list[str]:
|
|
210
|
-
"""Split
|
|
211
|
+
"""Split colon-delimited values while preserving quoted or escaped colons."""
|
|
211
212
|
return split_delimited_values(value, separators=":", allow_whitespace=True)
|
|
212
213
|
|
|
213
214
|
|
|
@@ -221,7 +222,7 @@ def parse_container_build_args(
|
|
|
221
222
|
env_build_args: str | None,
|
|
222
223
|
) -> dict[str, str] | None:
|
|
223
224
|
"""Parse container build args from CLI or environment input."""
|
|
224
|
-
if cli_build_args
|
|
225
|
+
if cli_build_args and any(token.strip() for token in cli_build_args):
|
|
225
226
|
build_arg_tokens = [
|
|
226
227
|
token.strip() for token in cli_build_args if token and token.strip()
|
|
227
228
|
]
|
|
@@ -233,10 +234,15 @@ def parse_container_build_args(
|
|
|
233
234
|
parsed_build_args: dict[str, str] = {}
|
|
234
235
|
for token in build_arg_tokens:
|
|
235
236
|
if "=" not in token:
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
utils.fatal(
|
|
238
|
+
f"Invalid build-arg token {token!r}; expected KEY=VALUE. "
|
|
239
|
+
"Escape literal colons in values as \\: or quote the value."
|
|
240
|
+
)
|
|
238
241
|
key, value = token.split("=", 1)
|
|
239
|
-
|
|
242
|
+
key = key.strip()
|
|
243
|
+
if not key:
|
|
244
|
+
utils.fatal(f"Invalid build-arg token {token!r}; the key must not be empty")
|
|
245
|
+
parsed_build_args[key] = value.strip()
|
|
240
246
|
|
|
241
247
|
return parsed_build_args or None
|
|
242
248
|
|
|
@@ -254,13 +260,20 @@ def parse_container_env_tokens(value: str | list[str] | None) -> list[str]:
|
|
|
254
260
|
return []
|
|
255
261
|
|
|
256
262
|
if isinstance(value, list):
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if not value.strip():
|
|
263
|
+
tokens = [token.strip() for token in value if token and token.strip()]
|
|
264
|
+
elif not value.strip():
|
|
260
265
|
return []
|
|
266
|
+
else:
|
|
267
|
+
tokens = split_colon_delimited_values(value)
|
|
261
268
|
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
for token in tokens:
|
|
270
|
+
if "=" not in token or not token.split("=", 1)[0].strip():
|
|
271
|
+
utils.fatal(
|
|
272
|
+
f"Invalid container env token {token!r}; expected KEY=VALUE. "
|
|
273
|
+
"Escape literal colons in values as \\: or quote the value."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return tokens
|
|
264
277
|
|
|
265
278
|
|
|
266
279
|
def _parse_container_envfile_paths(
|
common_python_tasks/github.py
CHANGED
|
@@ -3,6 +3,8 @@ import logging
|
|
|
3
3
|
import mimetypes
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from functools import lru_cache
|
|
6
8
|
from pathlib import Path
|
|
7
9
|
from typing import Any, Literal
|
|
8
10
|
|
|
@@ -39,7 +41,7 @@ def should_publish_github_release() -> bool:
|
|
|
39
41
|
"""Return whether GitHub Releases publication is enabled.
|
|
40
42
|
|
|
41
43
|
Returns:
|
|
42
|
-
True if GitHub Release publishing is enabled, False if it is skipped.
|
|
44
|
+
`True` if GitHub Release publishing is enabled, `False` if it is skipped.
|
|
43
45
|
"""
|
|
44
46
|
return not env_truthy("SKIP_GITHUB_RELEASE")
|
|
45
47
|
|
|
@@ -51,13 +53,25 @@ def get_github_repository() -> str | None:
|
|
|
51
53
|
The GitHub repository slug in the form `owner/repo`, or `None` if it
|
|
52
54
|
cannot be determined.
|
|
53
55
|
"""
|
|
54
|
-
|
|
56
|
+
return _get_github_repository(
|
|
57
|
+
os.getenv("GITHUB_REPOSITORY", "").strip(),
|
|
58
|
+
Path.cwd(),
|
|
59
|
+
utils.run_command,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@lru_cache(maxsize=32)
|
|
64
|
+
def _get_github_repository(
|
|
65
|
+
repository: str,
|
|
66
|
+
_working_directory: Path,
|
|
67
|
+
run_command: Callable[..., Any],
|
|
68
|
+
) -> str | None:
|
|
55
69
|
if repository:
|
|
56
70
|
if re.fullmatch(r"[^/]+/[^/]+", repository):
|
|
57
71
|
return repository
|
|
58
72
|
LOGGER.warning("Ignoring invalid GITHUB_REPOSITORY value: %s", repository)
|
|
59
73
|
|
|
60
|
-
result =
|
|
74
|
+
result = run_command(
|
|
61
75
|
["git", "remote", "get-url", "origin"],
|
|
62
76
|
capture_output=True,
|
|
63
77
|
acceptable_returncodes={0, 128},
|
|
@@ -94,8 +108,13 @@ def get_github_token() -> str | None:
|
|
|
94
108
|
if token:
|
|
95
109
|
return token
|
|
96
110
|
|
|
111
|
+
return _get_github_cli_token(utils.run_command)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@lru_cache(maxsize=8)
|
|
115
|
+
def _get_github_cli_token(run_command: Callable[..., Any]) -> str | None:
|
|
97
116
|
try:
|
|
98
|
-
result =
|
|
117
|
+
result = run_command(
|
|
99
118
|
["gh", "auth", "token"],
|
|
100
119
|
capture_output=True,
|
|
101
120
|
acceptable_returncodes={0, 1},
|
|
@@ -132,13 +151,18 @@ def get_github_api_base_url() -> str:
|
|
|
132
151
|
The base URL for GitHub API requests, defaulting to GitHub.com API or
|
|
133
152
|
GitHub Enterprise API if configured.
|
|
134
153
|
"""
|
|
135
|
-
|
|
154
|
+
return _get_github_api_base_url(
|
|
155
|
+
os.getenv("GITHUB_API_URL", "").strip(),
|
|
156
|
+
os.getenv("GITHUB_SERVER_URL", "https://github.com").strip(),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@lru_cache(maxsize=32)
|
|
161
|
+
def _get_github_api_base_url(api_url: str, server_url: str) -> str:
|
|
136
162
|
if api_url:
|
|
137
163
|
return api_url.rstrip("/")
|
|
138
164
|
|
|
139
|
-
server_url = (
|
|
140
|
-
os.getenv("GITHUB_SERVER_URL", "https://github.com").strip().rstrip("/")
|
|
141
|
-
)
|
|
165
|
+
server_url = server_url.rstrip("/")
|
|
142
166
|
if server_url.endswith("github.com"):
|
|
143
167
|
return "https://api.github.com"
|
|
144
168
|
return f"{server_url}/api/v3"
|
|
@@ -156,7 +180,11 @@ def get_github_upload_api_base_url(api_base_url: str | None = None) -> str:
|
|
|
156
180
|
Returns:
|
|
157
181
|
The upload base URL for GitHub API requests.
|
|
158
182
|
"""
|
|
159
|
-
|
|
183
|
+
return _get_github_upload_api_base_url(api_base_url or get_github_api_base_url())
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@lru_cache(maxsize=32)
|
|
187
|
+
def _get_github_upload_api_base_url(api_base: str) -> str:
|
|
160
188
|
if api_base == "https://api.github.com":
|
|
161
189
|
return "https://uploads.github.com"
|
|
162
190
|
return api_base
|
|
@@ -447,7 +475,8 @@ def get_github_release_asset_paths(
|
|
|
447
475
|
asset_sources: Optional list of paths or glob patterns. When a string is
|
|
448
476
|
provided (including via `GITHUB_RELEASE_ASSETS`), values are
|
|
449
477
|
interpreted as colon-separated paths or glob patterns.
|
|
450
|
-
If not provided, the default value is `dist/*`.
|
|
478
|
+
If not provided, the default value is `dist/*`. An empty list
|
|
479
|
+
explicitly disables asset selection.
|
|
451
480
|
|
|
452
481
|
Returns:
|
|
453
482
|
A sorted list of existing asset paths.
|
|
@@ -485,7 +514,7 @@ def get_github_release_asset_paths(
|
|
|
485
514
|
elif path.exists():
|
|
486
515
|
LOGGER.warning("Ignoring non-file GitHub release asset path: %s", path)
|
|
487
516
|
|
|
488
|
-
if not asset_paths and explicit_assets:
|
|
517
|
+
if not asset_paths and explicit_assets and source_items:
|
|
489
518
|
utils.fatal(
|
|
490
519
|
"No GitHub Release assets were found for the configured asset paths: "
|
|
491
520
|
+ ":".join(source_items)
|
common_python_tasks/project.py
CHANGED
|
@@ -18,6 +18,11 @@ from . import utils
|
|
|
18
18
|
LOGGER = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
PACKAGE_MANAGER_ENV_VAR = "COMMON_PYTHON_TASKS_PACKAGE_MANAGER"
|
|
21
|
+
PACKAGE_MANAGER_ENV_VARS = (
|
|
22
|
+
PACKAGE_MANAGER_ENV_VAR,
|
|
23
|
+
"COMMON_PYTHON_TASKS_BUILD_TOOL",
|
|
24
|
+
"PACKAGE_MANAGER",
|
|
25
|
+
)
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
class PackageManager(StrEnum):
|
|
@@ -45,9 +50,10 @@ def _parse_tool_version_output(output: str, tool_name: str) -> str:
|
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
def _find_package_manager_override() -> tuple[str, str] | None:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
for env_var in PACKAGE_MANAGER_ENV_VARS:
|
|
54
|
+
value = os.getenv(env_var)
|
|
55
|
+
if value is not None and value.strip():
|
|
56
|
+
return env_var, value.strip().lower()
|
|
51
57
|
return None
|
|
52
58
|
|
|
53
59
|
|
|
@@ -105,7 +111,8 @@ def get_package_manager() -> PackageManager:
|
|
|
105
111
|
"""Resolve the package manager backend used for build and publish commands.
|
|
106
112
|
|
|
107
113
|
Resolution order:
|
|
108
|
-
1. Environment
|
|
114
|
+
1. Environment overrides via `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`,
|
|
115
|
+
`COMMON_PYTHON_TASKS_BUILD_TOOL`, or `PACKAGE_MANAGER`, in that order.
|
|
109
116
|
2. Auto-discovery from installed executables and project metadata.
|
|
110
117
|
|
|
111
118
|
Supported values are `poetry`, `uv`, and `auto`.
|
|
@@ -374,7 +381,13 @@ def read_pyproject_toml() -> dict[str, Any]:
|
|
|
374
381
|
return tomllib.loads(Path("pyproject.toml").read_text())
|
|
375
382
|
|
|
376
383
|
|
|
377
|
-
|
|
384
|
+
BLACK_TARGET_PYTHON_MINORS = range(7, 17)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _specifier_supports_python_minor(specifier_set: SpecifierSet, minor: int) -> bool:
|
|
388
|
+
return any(
|
|
389
|
+
specifier_set.contains(Version(f"3.{minor}.{patch}")) for patch in range(1000)
|
|
390
|
+
)
|
|
378
391
|
|
|
379
392
|
|
|
380
393
|
def get_black_target_version() -> str | None:
|
|
@@ -394,9 +407,9 @@ def get_black_target_version() -> str | None:
|
|
|
394
407
|
LOGGER.warning("Unable to parse requires-python specifier: %s", requires_python)
|
|
395
408
|
return None
|
|
396
409
|
|
|
397
|
-
for
|
|
398
|
-
if specifier_set
|
|
399
|
-
return f"
|
|
410
|
+
for minor in BLACK_TARGET_PYTHON_MINORS:
|
|
411
|
+
if _specifier_supports_python_minor(specifier_set, minor):
|
|
412
|
+
return f"py3{minor:02d}"
|
|
400
413
|
return None
|
|
401
414
|
|
|
402
415
|
|
|
@@ -413,22 +426,57 @@ def has_debug_dependency_group() -> bool:
|
|
|
413
426
|
return bool(debug_group)
|
|
414
427
|
|
|
415
428
|
|
|
416
|
-
def
|
|
429
|
+
def _get_project_scripts() -> dict[str, str]:
|
|
430
|
+
scripts = read_pyproject_toml().get("project", {}).get("scripts")
|
|
431
|
+
if not isinstance(scripts, dict):
|
|
432
|
+
return {}
|
|
433
|
+
|
|
434
|
+
normalized_scripts = {
|
|
435
|
+
str(script_name).strip(): str(script_target)
|
|
436
|
+
for script_name, script_target in scripts.items()
|
|
437
|
+
if str(script_name).strip()
|
|
438
|
+
}
|
|
439
|
+
return normalized_scripts
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _get_available_container_entrypoint_scripts() -> tuple[str, ...]:
|
|
443
|
+
return tuple(sorted(_get_project_scripts().keys()))
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def resolve_container_entrypoint_command(entrypoint_script: str | None = None) -> str:
|
|
417
447
|
"""Resolve the command used in the generated container entrypoint script.
|
|
418
448
|
|
|
419
449
|
Args:
|
|
420
|
-
|
|
450
|
+
entrypoint_script: Optional explicit entrypoint script override.
|
|
421
451
|
|
|
422
452
|
Returns:
|
|
423
453
|
A command name to place in `/pkg/entrypoint.sh`.
|
|
424
454
|
"""
|
|
425
455
|
|
|
426
|
-
|
|
427
|
-
|
|
456
|
+
available_scripts = _get_available_container_entrypoint_scripts()
|
|
457
|
+
scripts_set = set(available_scripts)
|
|
458
|
+
|
|
459
|
+
if entrypoint_script is not None and entrypoint_script.strip():
|
|
460
|
+
selected_entrypoint = entrypoint_script.strip()
|
|
461
|
+
if selected_entrypoint in scripts_set:
|
|
462
|
+
return selected_entrypoint
|
|
463
|
+
|
|
464
|
+
if available_scripts:
|
|
465
|
+
utils.fatal(
|
|
466
|
+
"Invalid CUSTOM_ENTRYPOINT value "
|
|
467
|
+
f"{selected_entrypoint!r}. "
|
|
468
|
+
"It must match a key in [project].scripts. "
|
|
469
|
+
f"Available scripts: {', '.join(available_scripts)}"
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
utils.fatal(
|
|
473
|
+
"Invalid CUSTOM_ENTRYPOINT value "
|
|
474
|
+
f"{selected_entrypoint!r}. "
|
|
475
|
+
"No [project].scripts entries were found in pyproject.toml."
|
|
476
|
+
)
|
|
428
477
|
|
|
429
478
|
script_name = utils.get_package_name(use_underscores=True)
|
|
430
|
-
|
|
431
|
-
if isinstance(scripts, dict) and script_name in scripts:
|
|
479
|
+
if script_name in scripts_set:
|
|
432
480
|
return script_name
|
|
433
481
|
|
|
434
482
|
return "python"
|
common_python_tasks/tasks.py
CHANGED
|
@@ -582,7 +582,7 @@ def build_image(
|
|
|
582
582
|
|
|
583
583
|
apt_packages = (parsed_build_args or {}).get("APT_PACKAGES", "").strip()
|
|
584
584
|
entrypoint_command = resolve_container_entrypoint_command(
|
|
585
|
-
(parsed_build_args or {}).get("CUSTOM_ENTRYPOINT")
|
|
585
|
+
entrypoint_script=(parsed_build_args or {}).get("CUSTOM_ENTRYPOINT")
|
|
586
586
|
)
|
|
587
587
|
container_env_vars = load_container_env_tokens(
|
|
588
588
|
container_env,
|
|
@@ -884,7 +884,7 @@ def run_container(
|
|
|
884
884
|
f"No local images found for {package_name}. Build the image first or specify a tag."
|
|
885
885
|
)
|
|
886
886
|
|
|
887
|
-
LOGGER.
|
|
887
|
+
LOGGER.debug("Running container %s", selected_image)
|
|
888
888
|
run_args = ["docker", "run", "--rm", "-i", "-t"]
|
|
889
889
|
if privileged:
|
|
890
890
|
run_args.append("--privileged")
|
|
@@ -1000,7 +1000,8 @@ def publish_github_release(
|
|
|
1000
1000
|
release_name or os.getenv("GITHUB_RELEASE_NAME") or resolved_tag_name
|
|
1001
1001
|
)
|
|
1002
1002
|
resolved_body = body or os.getenv("GITHUB_RELEASE_BODY")
|
|
1003
|
-
|
|
1003
|
+
# Poe supplies `[]` when an optional repeated CLI argument is omitted.
|
|
1004
|
+
asset_paths = get_github_release_asset_paths(assets or None)
|
|
1004
1005
|
|
|
1005
1006
|
return publish_github_release_helper(
|
|
1006
1007
|
resolved_tag_name,
|
|
@@ -1088,6 +1089,26 @@ def changelog() -> None:
|
|
|
1088
1089
|
print(_changelog())
|
|
1089
1090
|
|
|
1090
1091
|
|
|
1092
|
+
def _build_and_publish_release_package(release_tag: str) -> None:
|
|
1093
|
+
from .utils import run_command
|
|
1094
|
+
|
|
1095
|
+
package_published = False
|
|
1096
|
+
try:
|
|
1097
|
+
build_package(clean_dist=True)
|
|
1098
|
+
publish_package(build_first=False)
|
|
1099
|
+
package_published = True
|
|
1100
|
+
finally:
|
|
1101
|
+
if not package_published:
|
|
1102
|
+
LOGGER.warning(
|
|
1103
|
+
"Package build or publication failed; deleting local release tag %s",
|
|
1104
|
+
release_tag,
|
|
1105
|
+
)
|
|
1106
|
+
run_command(
|
|
1107
|
+
["git", "tag", "--delete", release_tag],
|
|
1108
|
+
acceptable_returncodes={0, 1},
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
|
|
1091
1112
|
def _run_release_flow(
|
|
1092
1113
|
component: str = "auto",
|
|
1093
1114
|
stage: str | None = None,
|
|
@@ -1167,8 +1188,7 @@ def _run_release_flow(
|
|
|
1167
1188
|
run_command(["sh", "-lc", post_script], env=hook_env, dry_run=False)
|
|
1168
1189
|
return
|
|
1169
1190
|
|
|
1170
|
-
|
|
1171
|
-
publish_package(build_first=False)
|
|
1191
|
+
_build_and_publish_release_package(hook_env["RELEASE_TAG"])
|
|
1172
1192
|
|
|
1173
1193
|
if include_containers:
|
|
1174
1194
|
build_image(
|
|
@@ -1410,6 +1430,7 @@ def fastapi_stack_up(
|
|
|
1410
1430
|
from .env import (
|
|
1411
1431
|
get_cache_id_suffix,
|
|
1412
1432
|
load_container_env_tokens,
|
|
1433
|
+
parse_container_build_args,
|
|
1413
1434
|
)
|
|
1414
1435
|
from .project import (
|
|
1415
1436
|
get_package_manager,
|
|
@@ -1429,6 +1450,13 @@ def fastapi_stack_up(
|
|
|
1429
1450
|
)
|
|
1430
1451
|
|
|
1431
1452
|
container_env_vars = load_container_env_tokens(container_env, container_envfile)
|
|
1453
|
+
parsed_build_args = parse_container_build_args(
|
|
1454
|
+
build_args,
|
|
1455
|
+
os.getenv("CONTAINER_BUILD_ARGS"),
|
|
1456
|
+
)
|
|
1457
|
+
entrypoint_command = resolve_container_entrypoint_command(
|
|
1458
|
+
entrypoint_script=(parsed_build_args or {}).get("CUSTOM_ENTRYPOINT")
|
|
1459
|
+
)
|
|
1432
1460
|
|
|
1433
1461
|
cache_id_suffix = get_cache_id_suffix(no_cache)
|
|
1434
1462
|
|
|
@@ -1439,7 +1467,7 @@ def fastapi_stack_up(
|
|
|
1439
1467
|
"EXTENSION_CONTENT": "",
|
|
1440
1468
|
"HAS_DEBUG_DEPS": has_debug_deps,
|
|
1441
1469
|
"APT_PACKAGES": "",
|
|
1442
|
-
"ENTRYPOINT_COMMAND":
|
|
1470
|
+
"ENTRYPOINT_COMMAND": entrypoint_command,
|
|
1443
1471
|
"CONTAINER_ENV_VARS": container_env_vars,
|
|
1444
1472
|
"CACHE_ID_SUFFIX": cache_id_suffix,
|
|
1445
1473
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: common-python-tasks
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -46,14 +46,16 @@ Instead of writing your own tasks for formatting, linting, testing, packaging, a
|
|
|
46
46
|
You can add `common-python-tasks` to a new project by using the handy automated installation script.
|
|
47
47
|
|
|
48
48
|
```shell
|
|
49
|
-
curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/
|
|
49
|
+
curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/add_common_python_tasks.py | jq -r '.content' | base64 -d | python3
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
To install a specific release, set the environment variable `COMMON_PYTHON_TASKS_VERSION`.
|
|
53
53
|
|
|
54
54
|
```shell
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
curl -sSL https://api.github.com/repos/ci-sourcerer/common-python-tasks/contents/scripts/add_common_python_tasks.py \
|
|
56
|
+
| jq -r '.content' \
|
|
57
|
+
| base64 -d \
|
|
58
|
+
| COMMON_PYTHON_TASKS_VERSION=0.5.1 python3
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
This will complete the following steps.
|
|
@@ -73,7 +75,7 @@ There's no real reason to run the automated script; I just like automating every
|
|
|
73
75
|
```toml
|
|
74
76
|
[project]
|
|
75
77
|
dependencies = [
|
|
76
|
-
"common-python-tasks==0.
|
|
78
|
+
"common-python-tasks==0.5.1" # Always pin to a specific version
|
|
77
79
|
]
|
|
78
80
|
|
|
79
81
|
[tool.poe]
|
|
@@ -220,7 +222,7 @@ Supported values are `poetry`, `uv`, and `auto`.
|
|
|
220
222
|
|
|
221
223
|
Tasks that need configuration files (`pytest`, `coverage`, `flake8`, `isort`) follow this order of precedence.
|
|
222
224
|
|
|
223
|
-
1. **`pyproject.toml` sections**: `[tool.pytest]`, `[tool.coverage]`, `[tool.isort]` take priority
|
|
225
|
+
1. **`pyproject.toml` sections**: `[tool.pytest.ini_options]`, `[tool.coverage]`, `[tool.isort]` take priority
|
|
224
226
|
2. **Environment variables**: Override config paths; see [Environment Variables](#environment-variables)
|
|
225
227
|
3. **Local config files**: `pytest.ini`, `.coveragerc`, `.flake8`, `.isort.cfg` in the project root
|
|
226
228
|
4. **Bundled defaults**: Sensible defaults included with this package, found in the [`src/common_python_tasks/data`](src/common_python_tasks/data) directory
|
|
@@ -248,19 +250,19 @@ The following environment variables configure package and container behavior.
|
|
|
248
250
|
- `PACKAGE_MANAGER`: Alias for `COMMON_PYTHON_TASKS_PACKAGE_MANAGER`
|
|
249
251
|
- `CONTAINER_REGISTRY_USERNAME`: Specifies the container registry username for image tagging; the default is the current local user
|
|
250
252
|
- `CONTAINER_REGISTRY_URL`: Specifies the registry URL (default: `docker.io/{username}`)
|
|
251
|
-
- `CONTAINER_BUILD_ARGS`: Provides additional Docker build arguments in `KEY=VALUE:OTHER=VALUE` format
|
|
252
|
-
- `CONTAINER_EXTENSION_FILES`: Specifies colon delimited local extension Dockerfile paths to include in the rendered build
|
|
253
|
-
- `CONTAINER_EXTENSIONS`: Specifies colon delimited extension bundle names or parameterized bundle values to include in the rendered build
|
|
254
|
-
- `CONTAINER_ENV`: Provides colon delimited `KEY=VALUE` declarations to inject into the builder stage of the rendered Dockerfile
|
|
253
|
+
- `CONTAINER_BUILD_ARGS`: Provides additional Docker build arguments in `KEY=VALUE:OTHER=VALUE` format. Escape literal colons in values as `\:` or quote the complete value
|
|
254
|
+
- `CONTAINER_EXTENSION_FILES`: Specifies colon delimited local extension Dockerfile paths to include in the rendered build; escape literal colons as `\:` or quote the complete path
|
|
255
|
+
- `CONTAINER_EXTENSIONS`: Specifies colon delimited extension bundle names or parameterized bundle values to include in the rendered build; escape literal colons as `\:` or quote the complete value
|
|
256
|
+
- `CONTAINER_ENV`: Provides colon delimited `KEY=VALUE` declarations to inject into the builder stage of the rendered Dockerfile. Escape literal colons in values as `\:` or quote the complete value
|
|
255
257
|
- `.containerenv`: Can also supply the same declarations from a file in the project root. It is loaded first, then `container_envfile`, then `CONTAINER_ENV`, and finally `container_env`.
|
|
256
258
|
- `CONTAINER_PRUNE_KEEP`: Controls image pruning after builds (`-1` keeps all, `0` keeps the latest only, `N` keeps the latest plus `N` previous)
|
|
257
|
-
- `CUSTOM_ENTRYPOINT`: Specifies a custom entrypoint script name for containers
|
|
259
|
+
- `CUSTOM_ENTRYPOINT`: Specifies a custom entrypoint script name for containers. The value must match a key in `[project].scripts`; unknown values fail the build
|
|
258
260
|
- `CONTAINER_DEPS_CONTENT`: Supplies inline Dockerfile instructions for a dependency image that installs artifacts into `/tmp/deps`
|
|
259
|
-
- `CONTAINER_DEPS_FILE`: Points to one or more explicit Dockerfiles to build the dependency image. It may be a colon delimited list of file paths and is used only when `CONTAINER_DEPS_CONTENT` is unset
|
|
261
|
+
- `CONTAINER_DEPS_FILE`: Points to one or more explicit Dockerfiles to build the dependency image. It may be a colon delimited list of file paths, with literal colons escaped as `\:`, and is used only when `CONTAINER_DEPS_CONTENT` is unset
|
|
260
262
|
- `CONTAINER_DEPS_MAPPINGS`: Maps copied dependency names from `/tmp/deps` into destination paths as space separated `name:/target/path` entries. This is used only when `CONTAINER_DEPS_MOVE_SCRIPT` or `CONTAINER_DEPS_MOVE_SCRIPT_PATH` is not set
|
|
261
263
|
- `CONTAINER_DEPS_MOVE_SCRIPT`: Supplies a raw executable script to run after `/tmp/deps` is copied into the image. The script is written to `/tmp/container-deps-move-script` and executed with its own shebang
|
|
262
264
|
- `CONTAINER_DEPS_MOVE_SCRIPT_PATH`: Supplies a host path to a script file to run after `/tmp/deps` is copied into the image. This path takes precedence over `CONTAINER_DEPS_MOVE_SCRIPT` when both are set
|
|
263
|
-
- `GITHUB_RELEASE_ASSETS`: Colon separated list of file paths or glob patterns to attach to the GitHub Release (default: `dist/*`)
|
|
265
|
+
- `GITHUB_RELEASE_ASSETS`: Colon separated list of file paths or glob patterns to attach to the GitHub Release (default: `dist/*`); escape literal colons as `\:` or quote the complete path
|
|
264
266
|
- `SKIP_GITHUB_RELEASE`: Truthy value to skip GitHub Release publication
|
|
265
267
|
- `GITHUB_TOKEN` or `GH_TOKEN`: GitHub authentication token used to publish releases and upload assets
|
|
266
268
|
- `GITHUB_REPOSITORY`: Optional override for the repository slug used when publishing a GitHub Release
|
|
@@ -307,8 +309,8 @@ By default, `tasks()` exposes the common task set. You can still include or excl
|
|
|
307
309
|
```toml
|
|
308
310
|
[project]
|
|
309
311
|
name = "simple-cli-tool"
|
|
310
|
-
version = "0.
|
|
311
|
-
dependencies = ["common-python-tasks==0.
|
|
312
|
+
version = "0.5.1"
|
|
313
|
+
dependencies = ["common-python-tasks==0.5.1"]
|
|
312
314
|
|
|
313
315
|
[tool.poe]
|
|
314
316
|
include_script = "common_python_tasks:tasks()"
|
|
@@ -320,7 +322,7 @@ Available tasks: common defaults such as `format`, `lint`, `test`, and `build`.
|
|
|
320
322
|
|
|
321
323
|
```toml
|
|
322
324
|
[project]
|
|
323
|
-
dependencies = ["common-python-tasks==0.
|
|
325
|
+
dependencies = ["common-python-tasks==0.5.1"]
|
|
324
326
|
|
|
325
327
|
[tool.poe]
|
|
326
328
|
include_script = "common_python_tasks:tasks(include_tags=['common', 'containers'])"
|
|
@@ -336,7 +338,7 @@ Available tasks: All tasks including `build-image` and `push-image`.
|
|
|
336
338
|
|
|
337
339
|
```toml
|
|
338
340
|
[project]
|
|
339
|
-
dependencies = ["common-python-tasks==0.
|
|
341
|
+
dependencies = ["common-python-tasks==0.5.1"]
|
|
340
342
|
|
|
341
343
|
[tool.poe]
|
|
342
344
|
include_script = "common_python_tasks:tasks()"
|
|
@@ -424,7 +426,7 @@ The standard Python Dockerfile incorporates several intentional design choices.
|
|
|
424
426
|
- 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
|
|
425
427
|
- 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
|
|
426
428
|
- 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
|
|
427
|
-
- Safe entrypoint selection: The default entrypoint resolves the console script matching the package name
|
|
429
|
+
- Safe entrypoint selection: The default entrypoint resolves the console script matching the package name and falls back to `python` when no matching script exists. `CUSTOM_ENTRYPOINT` allows an override at build time, and the override is validated strictly against `[project].scripts`
|
|
428
430
|
- Minimal final image: Uses the slim Python base by default, cleans wheel artifacts and caches, and sets `runtime` as the explicit final target so the debug stage remains optional
|
|
429
431
|
|
|
430
432
|
## Notes
|
|
@@ -2,11 +2,11 @@ common_python_tasks/__init__.py,sha256=A-qMF818PGbDxM_DH9l1HwQMV-tXn8CZZiVQy-F_s
|
|
|
2
2
|
common_python_tasks/__main__.py,sha256=AZiLLusMDs0rZIIBuCDbt6jABlQD-kEwIQIep7GxgEc,3019
|
|
3
3
|
common_python_tasks/docker.py,sha256=KQ_Ym6tungnYqkf1EyXgdLpMjLNX879SWXKM9tIrfEg,19694
|
|
4
4
|
common_python_tasks/docker_compose.py,sha256=UnTP5-Kw31ZzkMUeuml1GxHkHmNsX76-9uezq0qPVBQ,21057
|
|
5
|
-
common_python_tasks/env.py,sha256=
|
|
5
|
+
common_python_tasks/env.py,sha256=X-v2gX6SJluvkjU7M941UbrMZ73vHnxwLeaBWRUxeLc,15678
|
|
6
6
|
common_python_tasks/git.py,sha256=wJ3gePpWE4fhNJYZCEWTDFrhHqaOVkaQveHeiGIbCHo,11188
|
|
7
|
-
common_python_tasks/github.py,sha256=
|
|
8
|
-
common_python_tasks/project.py,sha256=
|
|
9
|
-
common_python_tasks/tasks.py,sha256=
|
|
7
|
+
common_python_tasks/github.py,sha256=dLlHf6MYIrs0iTlBXtGm4yYgkzCZ0kJj4265ChH1O0U,19170
|
|
8
|
+
common_python_tasks/project.py,sha256=5ZU1PxefUPaSjPEGhEW3kiBLGBgt6O0VGejmVEg486E,20769
|
|
9
|
+
common_python_tasks/tasks.py,sha256=UIf6OX5C0MvZbIPTsLfR0Tr47TBtaiGLsJKbezabMq4,54906
|
|
10
10
|
common_python_tasks/utils.py,sha256=ej4nKFPMFhu1wSgkL9x50DML_sPoPmR6aGcIMyAmfN8,19586
|
|
11
11
|
common_python_tasks/data/dockerfile_extensions/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
common_python_tasks/data/fastapi/alembic.ini.j2,sha256=IVXmnF1pJvW3o8dXBLIVhmng7DyXu_sWXx_W3VlAcGE,910
|
|
@@ -22,7 +22,7 @@ common_python_tasks/data/generic/Dockerfile.deps.j2,sha256=rCGsBwLHQIRsGFzDbiRi_
|
|
|
22
22
|
common_python_tasks/data/generic/Dockerfile.j2,sha256=UG-QxIuFT27j925LsghPl5Wqcw5wNsPPuqB6rcRngz0,7263
|
|
23
23
|
common_python_tasks/data/generic/cliff.toml,sha256=po1MnEGEEgrpyBz3XLWr0h7emXb6ASX63lrU2r6o240,77
|
|
24
24
|
common_python_tasks/data/generic/pytest.ini,sha256=RHak5QrLzKHvUBjFmaaiOYFrd_8YBRQubXEOT0NJfco,134
|
|
25
|
-
common_python_tasks-0.
|
|
26
|
-
common_python_tasks-0.
|
|
27
|
-
common_python_tasks-0.
|
|
28
|
-
common_python_tasks-0.
|
|
25
|
+
common_python_tasks-0.5.1.dist-info/METADATA,sha256=rfvEbPvkxBa35iWaLL0KfBDoIFWPjMpTOgjBbHfVTFE,22592
|
|
26
|
+
common_python_tasks-0.5.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
27
|
+
common_python_tasks-0.5.1.dist-info/licenses/LICENSE,sha256=RsSweM-DyzxFPx38fQOJqHrKyeT9Fiz8rO7yox-fOsw,1070
|
|
28
|
+
common_python_tasks-0.5.1.dist-info/RECORD,,
|
|
File without changes
|
{common_python_tasks-0.4.1.dist-info → common_python_tasks-0.5.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|