Integrate task runner into python tools
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""Safe, non-executing task discovery."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def display_name(value: str) -> str:
|
||||
return value.replace("_", " ")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Task:
|
||||
id: str
|
||||
group_id: str
|
||||
filename: str
|
||||
path: Path
|
||||
display_name: str
|
||||
description: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Group:
|
||||
id: str
|
||||
path: Path
|
||||
display_name: str
|
||||
tasks: tuple[Task, ...]
|
||||
|
||||
|
||||
class TaskCatalog:
|
||||
"""Discovers only direct Python children of non-hidden group directories."""
|
||||
|
||||
def __init__(self, task_root: Path) -> None:
|
||||
self.task_root = task_root
|
||||
|
||||
def groups(self) -> tuple[Group, ...]:
|
||||
if not self.task_root.is_dir():
|
||||
return ()
|
||||
|
||||
groups = []
|
||||
for group_path in sorted(self.task_root.iterdir(), key=lambda path: path.name):
|
||||
if (
|
||||
group_path.name.startswith((".", "_"))
|
||||
or group_path.is_symlink()
|
||||
or not group_path.is_dir()
|
||||
):
|
||||
continue
|
||||
tasks = tuple(self._tasks_in_group(group_path))
|
||||
groups.append(
|
||||
Group(
|
||||
id=group_path.name,
|
||||
path=group_path,
|
||||
display_name=display_name(group_path.name),
|
||||
tasks=tasks,
|
||||
)
|
||||
)
|
||||
return tuple(groups)
|
||||
|
||||
def group(self, group_id: str) -> Group | None:
|
||||
return next((group for group in self.groups() if group.id == group_id), None)
|
||||
|
||||
def task(self, task_id: str) -> Task | None:
|
||||
for group in self.groups():
|
||||
for task in group.tasks:
|
||||
if task.id == task_id:
|
||||
return task
|
||||
return None
|
||||
|
||||
def _tasks_in_group(self, group_path: Path) -> list[Task]:
|
||||
tasks = []
|
||||
for path in sorted(group_path.iterdir(), key=lambda candidate: candidate.name):
|
||||
if (
|
||||
path.name.startswith((".", "_"))
|
||||
or path.is_symlink()
|
||||
or not path.is_file()
|
||||
or path.suffix != ".py"
|
||||
):
|
||||
continue
|
||||
tasks.append(
|
||||
Task(
|
||||
id=f"{group_path.name}/{path.name}",
|
||||
group_id=group_path.name,
|
||||
filename=path.name,
|
||||
path=path,
|
||||
display_name=display_name(path.stem),
|
||||
description=read_description(path),
|
||||
)
|
||||
)
|
||||
return tasks
|
||||
|
||||
|
||||
def read_description(path: Path) -> str | None:
|
||||
"""Return the first docstring line without importing or executing a task."""
|
||||
|
||||
try:
|
||||
module = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||
except (OSError, SyntaxError, UnicodeDecodeError):
|
||||
return None
|
||||
docstring = ast.get_docstring(module, clean=True)
|
||||
if not docstring:
|
||||
return None
|
||||
return next(
|
||||
(line.strip() for line in docstring.splitlines() if line.strip()),
|
||||
None,
|
||||
)
|
||||
Reference in New Issue
Block a user