Refine Python tools task interface
Build custom container images / build (map[base_image:php:8-fpm-alpine build_args:PHP_VERSION=8 context:php8-pgsql fingerprint_command:{ apk info -v | LC_ALL=C sort; find /usr/local/lib/php/extensions /usr/local/etc/php/conf.d -type f -exec sha256sum {} + | LC_ALL=C sort; } name:ph… (push) Successful in 39s
Build custom container images / build (map[base_image:postgres:18 build_args:PG_VERSION=18 POSTGIS_VERSION=3 VCHORD_VERSION=0.5.3 context:postgres fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; find /usr/lib/postgresql -type f -exec sha256su… (push) Successful in 53s
Build custom container images / build (map[base_image:python:3 build_args:PYTHON_VERSION=3 context:python-tools fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:python-tools oci_labels:org.opencontainers.ima… (push) Successful in 1m17s

This commit is contained in:
ajp_anton
2026-08-18 04:21:11 +00:00
parent b256147d28
commit 7e6dea8b24
11 changed files with 311 additions and 95 deletions
+33 -14
View File
@@ -117,7 +117,12 @@ class TaskCatalog:
group_path,
{
path.name for path in group_path.iterdir()
if path.is_file() and not path.is_symlink() and path.suffix == ".py" and not path.name.startswith((".", "_"))
if (
path.is_file()
and not path.is_symlink()
and path.suffix == ".py"
and not path.name.startswith((".", "_"))
)
},
)
tasks = tuple(self._tasks_in_group(group_path, manifest.declarations))
@@ -125,7 +130,7 @@ class TaskCatalog:
Group(
id=group_path.name,
path=group_path,
display_name=manifest.label or display_name(group_path.name),
display_name=manifest.title or display_name(group_path.name),
tasks=tasks,
)
)
@@ -191,8 +196,12 @@ class TaskCatalog:
group_id=group_path.name,
filename=path.name,
path=path,
display_name=display_name(path.stem),
description=read_description(path),
display_name=declaration.title if declaration and declaration.title else display_name(path.stem),
description=(
declaration.description
if declaration and declaration.description is not None
else read_description(path)
),
inputs=(
(*declaration.inputs, declaration.execution.field)
if declaration and declaration.execution
@@ -287,7 +296,7 @@ def read_description(path: Path) -> str | None:
@dataclass(frozen=True)
class GroupManifest:
label: str | None
title: str | None
declarations: dict[str, "TaskDeclaration"]
@@ -299,22 +308,22 @@ def load_input_manifest(
manifest_path = group_path / "task-inputs.json"
if not manifest_path.exists():
return GroupManifest(label=None, declarations={})
return GroupManifest(title=None, declarations={})
if manifest_path.is_symlink():
raise ValueError(f"Input manifest cannot be a symlink: {manifest_path}")
try:
data = json.loads(manifest_path.read_text(encoding="utf-8"))
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
raise ValueError(f"Could not read input manifest {manifest_path}: {error}") from error
if not isinstance(data, dict) or set(data) - {"version", "label", "tasks"}:
if not isinstance(data, dict) or set(data) - {"version", "title", "tasks"}:
raise ValueError(
f"Input manifest {manifest_path} has an invalid structure."
)
if data.get("version") != 1 or not isinstance(data.get("tasks"), dict):
raise ValueError(f"Input manifest {manifest_path} has an unsupported structure.")
label = data.get("label")
if label is not None and (not isinstance(label, str) or not label.strip()):
raise ValueError(f"Input manifest {manifest_path} label must be non-empty text.")
title = data.get("title")
if title is not None and (not isinstance(title, str) or not title.strip()):
raise ValueError(f"Input manifest {manifest_path} title must be non-empty text.")
unknown_tasks = set(data["tasks"]) - task_filenames
if unknown_tasks:
@@ -322,7 +331,7 @@ def load_input_manifest(
raise ValueError(f"Input manifest {manifest_path} names unknown tasks: {names}.")
return GroupManifest(
label=label,
title=title,
declarations={
filename: parse_task_declaration(manifest_path, filename, declaration)
for filename, declaration in data["tasks"].items()
@@ -332,6 +341,8 @@ def load_input_manifest(
@dataclass(frozen=True)
class TaskDeclaration:
title: str | None
description: str | None
inputs: tuple[InputField, ...]
wait_for_result: bool
download_artifacts: bool
@@ -345,13 +356,19 @@ def parse_task_declaration(
) -> TaskDeclaration:
if (
not isinstance(declaration, dict)
or set(declaration) - {"inputs", "wait_for_result", "download_artifacts", "execution"}
or set(declaration) - {"title", "description", "inputs", "wait_for_result", "download_artifacts", "execution"}
):
raise ValueError(f"{manifest_path} task {filename} has an invalid declaration.")
raw_inputs = declaration.get("inputs", [])
if not isinstance(raw_inputs, list):
raise ValueError(f"{manifest_path} task {filename} inputs must be a list.")
fields = tuple(parse_input_field(manifest_path, filename, raw) for raw in raw_inputs)
title = declaration.get("title")
if title is not None and (not isinstance(title, str) or not title.strip()):
raise ValueError(f"{manifest_path} task {filename} title must be non-empty text.")
description = declaration.get("description")
if description is not None and (not isinstance(description, str) or not description.strip()):
raise ValueError(f"{manifest_path} task {filename} description must be non-empty text.")
names = [field.name for field in fields]
if len(names) != len(set(names)):
raise ValueError(f"{manifest_path} task {filename} has duplicate input names.")
@@ -365,6 +382,8 @@ def parse_task_declaration(
if download_artifacts and not wait_for_result:
raise ValueError(f"{manifest_path} task {filename} download_artifacts requires wait_for_result.")
return TaskDeclaration(
title=title,
description=description,
inputs=fields,
wait_for_result=wait_for_result,
download_artifacts=download_artifacts,
@@ -381,7 +400,7 @@ def parse_execution_mode(
if raw is None:
return None
required_keys = {
"field", "label", "dry_run_value", "dry_run_label", "execute_value", "execute_label",
"field", "dry_run_value", "dry_run_label", "execute_value", "execute_label",
}
if not isinstance(raw, dict) or set(raw) != required_keys:
raise ValueError(f"{manifest_path} task {filename} has an invalid execution declaration.")
@@ -395,7 +414,7 @@ def parse_execution_mode(
raise ValueError(f"{manifest_path} task {filename} execution values must differ.")
field = InputField(
name=field_name,
label=values["label"],
label="Run mode",
type="choice",
required=True,
default=values["dry_run_value"],