Expand Python maintenance tasks

This commit is contained in:
ajp_anton
2026-08-09 13:08:54 +00:00
parent 0dfb493ae1
commit dade2553d7
4 changed files with 64 additions and 10 deletions
+6 -1
View File
@@ -61,6 +61,7 @@ class InputField:
options: tuple[InputOption, ...]
accept: tuple[str, ...]
maximum_bytes: int | None
sensitive: bool = False
@dataclass(frozen=True)
@@ -325,7 +326,7 @@ def parse_input_field(manifest_path: Path, filename: str, raw: Any) -> InputFiel
raise ValueError(f"{manifest_path} task {filename} has a non-object input field.")
allowed = {
"name", "label", "type", "required", "default", "minimum", "maximum",
"step", "pattern", "options", "accept", "maximum_bytes",
"step", "pattern", "options", "accept", "maximum_bytes", "sensitive",
}
unknown = set(raw) - allowed
if unknown:
@@ -344,6 +345,9 @@ def parse_input_field(manifest_path: Path, filename: str, raw: Any) -> InputFiel
required = raw.get("required", False)
if not isinstance(required, bool):
raise ValueError(f"{manifest_path} task {filename} input {name} required must be true or false.")
sensitive = raw.get("sensitive", False)
if not isinstance(sensitive, bool):
raise ValueError(f"{manifest_path} task {filename} input {name} sensitive must be true or false.")
numeric_keys = ("minimum", "maximum", "step")
numeric_values = {key: raw.get(key) for key in numeric_keys}
@@ -407,6 +411,7 @@ def parse_input_field(manifest_path: Path, filename: str, raw: Any) -> InputFiel
options=options,
accept=accept,
maximum_bytes=maximum_bytes,
sensitive=sensitive,
)
+47 -4
View File
@@ -34,24 +34,30 @@ class RunManager:
input_data: dict[str, str | int | list[str]] | None = None,
uploads: Mapping[str, PendingUpload] | None = None,
) -> tuple[Run, bool]:
sensitive_names = {field.name for field in task.inputs if field.sensitive}
stored_input = self._redact_input(input_data or {}, sensitive_names)
run, created = self.store.enqueue(
target_kind="task",
target_path=task.id,
queue_group=task.group_id,
input_data=input_data,
input_data=stored_input,
)
if created:
try:
uploaded_inputs = self._save_uploads(run.id, uploads or {})
execution_input = {**(input_data or {}), **uploaded_inputs}
stored_input = self._redact_input(execution_input, sensitive_names)
run = self.store.set_input(run.id, stored_input)
if sensitive_names:
self._write_execution_input(run.id, execution_input)
except (OSError, ValueError) as error:
self._delete_uploads(run.id)
self._delete_execution_input(run.id)
return self.store.finish(
run.id,
status="failed",
message=f"Could not save uploaded file: {error}",
), True
if uploaded_inputs:
run = self.store.set_input(run.id, {**(input_data or {}), **uploaded_inputs})
self._wake_worker(task.group_id)
return run, created
@@ -77,6 +83,7 @@ class RunManager:
completed = self.store.get(run.id)
if completed is not None and completed.status == "cancelled":
self._delete_uploads(run.id)
self._delete_execution_input(run.id)
return completed
def artifacts(self, run_id: int) -> list[str]:
@@ -128,6 +135,13 @@ class RunManager:
continue
if expired:
shutil.rmtree(path, ignore_errors=True)
for path in runs_root.glob("*.execution-input.json"):
run_id_text = path.name.removesuffix(".execution-input.json")
if not run_id_text.isdigit():
continue
run = self.store.get(int(run_id_text))
if run is None or run.status not in {"queued", "running"}:
path.unlink(missing_ok=True)
def run_background_task(self, task: BackgroundTask) -> None:
"""Run a declared internal task without creating high-volume run history."""
@@ -239,10 +253,12 @@ class RunManager:
log_path = self.settings.state_root / "runs" / f"{run.id}.log"
log_path.parent.mkdir(parents=True, exist_ok=True)
input_path = self.settings.state_root / "runs" / f"{run.id}.input.json"
input_path = self._execution_input_path(run.id)
artifact_root = self._artifact_root(run.id)
artifact_root.mkdir(parents=True, exist_ok=True)
try:
if not input_path.exists():
input_path = self.settings.state_root / "runs" / f"{run.id}.input.json"
input_path.write_text(run.input_json + "\n", encoding="utf-8")
input_path.chmod(0o600)
except OSError as error:
@@ -284,6 +300,7 @@ class RunManager:
with self._lock:
self._processes.pop(run.id, None)
self._delete_uploads(run.id)
self._delete_execution_input(run.id)
if self.store.is_cancel_requested(run.id) or (
parent_run_id is not None and self.store.is_cancel_requested(parent_run_id)
@@ -330,6 +347,32 @@ class RunManager:
def _artifact_root(self, run_id: int) -> Path:
return self.settings.state_root / "runs" / f"{run_id}.artifacts"
def _execution_input_path(self, run_id: int) -> Path:
return self.settings.state_root / "runs" / f"{run_id}.execution-input.json"
def _write_execution_input(
self,
run_id: int,
input_data: Mapping[str, str | int | list[str]],
) -> None:
path = self._execution_input_path(run_id)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(input_data, sort_keys=True, separators=(",", ":")) + "\n", encoding="utf-8")
path.chmod(0o600)
def _delete_execution_input(self, run_id: int) -> None:
self._execution_input_path(run_id).unlink(missing_ok=True)
@staticmethod
def _redact_input(
input_data: Mapping[str, str | int | list[str]],
sensitive_names: set[str],
) -> dict[str, str | int | list[str]]:
return {
name: "[redacted]" if name in sensitive_names else value
for name, value in input_data.items()
}
def _save_uploads(
self,
run_id: int,
+6 -1
View File
@@ -102,7 +102,12 @@ def create_app(settings: Settings | None = None) -> Flask:
if request.accept_mimetypes.best == "application/json":
return {"errors": errors}, 400
return render_index(
form_values={task.id: submitted_values},
form_values={
task.id: {
field.name: [] if field.sensitive else submitted_values[field.name]
for field in task.inputs
}
},
input_errors={task.id: errors},
status=400,
)
+3 -2
View File
@@ -68,12 +68,13 @@
</select>
</label>
{% else %}
{% set html_type = "number" if field.type == "integer" else "datetime-local" if field.type == "datetime" else field.type %}
{% set html_type = "password" if field.sensitive else "number" if field.type == "integer" else "datetime-local" if field.type == "datetime" else field.type %}
<label>{{ field.label }}{% if field.required %} (required){% endif %}
<input
type="{{ html_type }}"
name="{{ field.name }}"
value="{{ values.get(field.name, field.default if field.default is not none else "") }}"
value="{{ "" if field.sensitive else values.get(field.name, field.default if field.default is not none else "") }}"
{% if field.sensitive %}autocomplete="off"{% endif %}
{% if field.required %}required{% endif %}
{% if field.minimum is not none %}min="{{ field.minimum }}"{% endif %}
{% if field.maximum is not none %}max="{{ field.maximum }}"{% endif %}