potent 0.1.0__tar.gz
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.
- potent-0.1.0/PKG-INFO +15 -0
- potent-0.1.0/README.md +3 -0
- potent-0.1.0/pyproject.toml +56 -0
- potent-0.1.0/src/potent/__init__.py +25 -0
- potent-0.1.0/src/potent/commands/_types.py +19 -0
- potent-0.1.0/src/potent/commands/dump_schema.py +15 -0
- potent-0.1.0/src/potent/commands/reset.py +17 -0
- potent-0.1.0/src/potent/commands/run.py +80 -0
- potent-0.1.0/src/potent/commands/validate.py +24 -0
- potent-0.1.0/src/potent/directives/_base.py +81 -0
- potent-0.1.0/src/potent/directives/clean_workdir.py +23 -0
- potent-0.1.0/src/potent/directives/git_pull.py +21 -0
- potent-0.1.0/src/potent/directives/switch_branch.py +49 -0
- potent-0.1.0/src/potent/shellprint.py +132 -0
- potent-0.1.0/src/potent/util.py +5 -0
potent-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: potent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author: David Brownman
|
|
6
|
+
Author-email: David Brownman <oss@xavd.id>
|
|
7
|
+
Requires-Dist: pydantic>=2.11.9
|
|
8
|
+
Requires-Dist: rich>=14.1.0
|
|
9
|
+
Requires-Dist: typer>=0.19.2
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# potent
|
|
14
|
+
|
|
15
|
+
A CLI for running idem**potent** shell scripts.
|
potent-0.1.0/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "potent"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Add your description here"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "David Brownman", email = "oss@xavd.id" }]
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = ["pydantic>=2.11.9", "rich>=14.1.0", "typer>=0.19.2"]
|
|
9
|
+
|
|
10
|
+
[project.scripts]
|
|
11
|
+
potent = "potent:main"
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["uv_build>=0.8.4,<0.9.0"]
|
|
15
|
+
build-backend = "uv_build"
|
|
16
|
+
|
|
17
|
+
[tool.ruff.lint]
|
|
18
|
+
select = [
|
|
19
|
+
"E", # PEP8 recommendations
|
|
20
|
+
"F", # bugs
|
|
21
|
+
"I001", # import sorting
|
|
22
|
+
"BLE", # catching root Exception
|
|
23
|
+
"A", # built-in shadowing
|
|
24
|
+
"C4", # unnecessary comprehensions
|
|
25
|
+
"ISC", # implicit string concat
|
|
26
|
+
"PIE", # misc useful lints
|
|
27
|
+
"Q", # better quoting behavior
|
|
28
|
+
"RSE", # no parens on exceptions that lack args
|
|
29
|
+
"RET", # more correct return behavior
|
|
30
|
+
"SIM", # general style things
|
|
31
|
+
"TC", # type-only imports should be in a typecheck block
|
|
32
|
+
"ARG", # unused args
|
|
33
|
+
"PTH", # use pathlib
|
|
34
|
+
"FLY", # short "".join calls
|
|
35
|
+
"PERF", # performance hints
|
|
36
|
+
"PL", # pylint, general recommendations
|
|
37
|
+
# "RUF", # these are a little picky for me
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
ignore = [
|
|
41
|
+
"E501", # skip enforcing line length
|
|
42
|
+
"E741", # ignore short variable names
|
|
43
|
+
"PLR2004", # magic values
|
|
44
|
+
"PLR0911", # too many returns
|
|
45
|
+
"PLR0912", # too many branches
|
|
46
|
+
"PLR0913", # too many arguments
|
|
47
|
+
'Q000', # single quotes; handled by formatter
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
unfixable = [
|
|
51
|
+
"F401", # don't remove unused imports
|
|
52
|
+
"F841", # don't remove unused variables
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[dependency-groups]
|
|
56
|
+
dev = ["pytest>=8.4.2", "ruff>=0.13.3"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from potent.commands.dump_schema import app as dump_schema
|
|
4
|
+
from potent.commands.reset import app as reset
|
|
5
|
+
from potent.commands.run import app as run
|
|
6
|
+
from potent.commands.validate import app as validate
|
|
7
|
+
|
|
8
|
+
# COMMAND IMPORTS ^
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
help="Idempotently run commands across folders.",
|
|
13
|
+
# hide the completion args
|
|
14
|
+
add_completion=False,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
app.add_typer(validate)
|
|
18
|
+
app.add_typer(dump_schema)
|
|
19
|
+
app.add_typer(run)
|
|
20
|
+
app.add_typer(reset)
|
|
21
|
+
# COMMANDS ^
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
app()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_plan_json(_ctx: typer.Context, _param: typer.CallbackParam, value: Path):
|
|
8
|
+
if value.suffixes != [".plan", ".json"]:
|
|
9
|
+
raise typer.BadParameter("File must have .plan.json extension")
|
|
10
|
+
return value
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# important that this _doesn't_ have a `type PlainJson =` declaration
|
|
14
|
+
PlanJson = Annotated[
|
|
15
|
+
Path,
|
|
16
|
+
typer.Argument(
|
|
17
|
+
exists=True, dir_okay=False, resolve_path=True, callback=is_plan_json
|
|
18
|
+
),
|
|
19
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from potent.shellprint import Shellprint
|
|
7
|
+
|
|
8
|
+
app = typer.Typer()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command()
|
|
12
|
+
def dump_schema():
|
|
13
|
+
Path(__file__, "..", "..", "..", "..", "schema.json").resolve().write_text(
|
|
14
|
+
json.dumps(Shellprint.model_json_schema(), indent=2)
|
|
15
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from potent.commands._types import PlanJson
|
|
4
|
+
from potent.shellprint import Shellprint
|
|
5
|
+
|
|
6
|
+
app = typer.Typer()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@app.command()
|
|
10
|
+
def reset(path: PlanJson):
|
|
11
|
+
"""
|
|
12
|
+
Resets a plan file so it can be run again from scratch.
|
|
13
|
+
"""
|
|
14
|
+
with path.open("r+") as plan_file:
|
|
15
|
+
plan = Shellprint.from_file(plan_file)
|
|
16
|
+
plan.reset()
|
|
17
|
+
plan.save(plan_file)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from subprocess import CalledProcessError
|
|
3
|
+
from time import sleep
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
# from rich.live import Live
|
|
10
|
+
from potent.commands._types import PlanJson
|
|
11
|
+
from potent.shellprint import Shellprint
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def directory_header(console: Console, directory: Path) -> None:
|
|
17
|
+
return console.rule(
|
|
18
|
+
f"📂 [bold underline]{directory.name}[/] 📂", style="bright_cyan"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command()
|
|
23
|
+
def run(path: PlanJson):
|
|
24
|
+
# TODO: probably make this internal to the class??
|
|
25
|
+
# can maybe use a generator so the presentation is controlled in the CLI
|
|
26
|
+
plan = Shellprint.from_path(path)
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
console.print(f"Running [bold yellow]{str(path)}")
|
|
30
|
+
|
|
31
|
+
with path.open("r+") as plan_file:
|
|
32
|
+
for directory in plan.directories:
|
|
33
|
+
console.print()
|
|
34
|
+
if plan.directory_complete(directory):
|
|
35
|
+
directory_header(console, directory)
|
|
36
|
+
|
|
37
|
+
# directory_spinner.ok(f"☑️ {directory.name}")
|
|
38
|
+
console.print("☑️ [green]already finished")
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
directory_header(console, directory)
|
|
43
|
+
|
|
44
|
+
for step in plan.steps:
|
|
45
|
+
success = None
|
|
46
|
+
output = ""
|
|
47
|
+
style = ""
|
|
48
|
+
if step.completed(directory):
|
|
49
|
+
output = "Already completed"
|
|
50
|
+
subtitle = "skipped"
|
|
51
|
+
else:
|
|
52
|
+
result = step.run(directory)
|
|
53
|
+
plan.save(plan_file)
|
|
54
|
+
if success := result.success:
|
|
55
|
+
style = "green"
|
|
56
|
+
subtitle = "Succeeded"
|
|
57
|
+
else:
|
|
58
|
+
style = "red"
|
|
59
|
+
subtitle = "Failed"
|
|
60
|
+
|
|
61
|
+
output = result.output or "[dim]no output"
|
|
62
|
+
|
|
63
|
+
console.print(
|
|
64
|
+
Panel(
|
|
65
|
+
f"\n{output.strip()}\n",
|
|
66
|
+
title=f"[dim white]step[not dim]: {step.slug}",
|
|
67
|
+
title_align="left",
|
|
68
|
+
border_style=style,
|
|
69
|
+
subtitle=f"[dim white]result:[/] {subtitle}",
|
|
70
|
+
subtitle_align="left",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
console.print()
|
|
74
|
+
if success is False:
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
except NotImplementedError:
|
|
78
|
+
# directory_spinner.fail("ERR")
|
|
79
|
+
print(" err!")
|
|
80
|
+
continue
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import rich
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
from potent.commands._types import PlanJson
|
|
5
|
+
from potent.shellprint import Shellprint
|
|
6
|
+
|
|
7
|
+
app = typer.Typer()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command()
|
|
11
|
+
def validate(path: PlanJson):
|
|
12
|
+
# can handle this pretty cleanly by pegging the step location and the error type (missing, etc)
|
|
13
|
+
# probably want to hide the traceback though
|
|
14
|
+
# and maybe return
|
|
15
|
+
# can raise typer.Exit(code=int)
|
|
16
|
+
# print(f"Shellprint @ {str(path)}")
|
|
17
|
+
# print(Shellprint.from_path(path))
|
|
18
|
+
rich.print(Shellprint.from_path(path).summarize(path))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# could try to wrap, but the basic error is pretty good:
|
|
22
|
+
# from pydantic import ValidationError
|
|
23
|
+
# except ValidationError as e:
|
|
24
|
+
# print(e.errors()[0])
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Annotated, Literal, Optional, final
|
|
4
|
+
|
|
5
|
+
from pydantic import AfterValidator, BaseModel, DirectoryPath
|
|
6
|
+
|
|
7
|
+
from potent.util import truthy_list
|
|
8
|
+
|
|
9
|
+
Status = Literal["not-started", "failed", "completed"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# useful for writing back absolute paths, which is good for portability
|
|
13
|
+
def make_abs_path(value: Path) -> Path:
|
|
14
|
+
if not value.is_absolute():
|
|
15
|
+
raise ValueError("path is not absolute")
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
AbsPath = Annotated[
|
|
20
|
+
DirectoryPath,
|
|
21
|
+
AfterValidator(make_abs_path),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DirectiveResult(BaseModel):
|
|
26
|
+
success: bool
|
|
27
|
+
output: str
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def from_process(result: subprocess.CompletedProcess[str]):
|
|
31
|
+
return DirectiveResult(success=result.returncode == 0, output=result.stdout)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EmptyConfig(BaseModel):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseDirective(BaseModel):
|
|
39
|
+
comment: Optional[str] = None
|
|
40
|
+
directory_statuses: dict[AbsPath, Status] = {}
|
|
41
|
+
config: EmptyConfig = EmptyConfig()
|
|
42
|
+
|
|
43
|
+
@final
|
|
44
|
+
def run(self, directory: Path) -> DirectiveResult:
|
|
45
|
+
# try:
|
|
46
|
+
result = self._run(directory)
|
|
47
|
+
# except Exception: # noqa: BLE001
|
|
48
|
+
# success = False
|
|
49
|
+
|
|
50
|
+
self.directory_statuses[directory] = "completed" if result.success else "failed"
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def _run(self, directory: Path) -> DirectiveResult:
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
def reset(self) -> None:
|
|
57
|
+
self.directory_statuses = {}
|
|
58
|
+
|
|
59
|
+
def completed(self, directory: Path) -> bool:
|
|
60
|
+
return self.directory_statuses.get(directory) == "completed"
|
|
61
|
+
|
|
62
|
+
def failed(self, directory: Path) -> bool:
|
|
63
|
+
return self.directory_statuses.get(directory) == "failed"
|
|
64
|
+
|
|
65
|
+
def initialize_dirs(self, directories: list[Path]) -> None:
|
|
66
|
+
self.directory_statuses |= dict.fromkeys(directories, "not-started")
|
|
67
|
+
|
|
68
|
+
def _run_cmd(
|
|
69
|
+
self, directory: Path, cmd: list[str]
|
|
70
|
+
) -> subprocess.CompletedProcess[str]:
|
|
71
|
+
"""
|
|
72
|
+
helper for shelling out
|
|
73
|
+
"""
|
|
74
|
+
return subprocess.run(
|
|
75
|
+
truthy_list(cmd),
|
|
76
|
+
cwd=directory,
|
|
77
|
+
check=False,
|
|
78
|
+
stdout=subprocess.PIPE,
|
|
79
|
+
stderr=subprocess.STDOUT,
|
|
80
|
+
text=True,
|
|
81
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from time import sleep
|
|
3
|
+
from typing import Literal, override
|
|
4
|
+
|
|
5
|
+
from potent.directives._base import BaseDirective, DirectiveResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CleanWorkdir(BaseDirective):
|
|
9
|
+
slug: Literal["clean-status"]
|
|
10
|
+
|
|
11
|
+
@override
|
|
12
|
+
def _run(self, directory: Path) -> DirectiveResult:
|
|
13
|
+
result = self._run_cmd(directory, ["git", "status", "--porcelain"])
|
|
14
|
+
sleep(2)
|
|
15
|
+
|
|
16
|
+
success = not result.stdout
|
|
17
|
+
|
|
18
|
+
return DirectiveResult(
|
|
19
|
+
success=success,
|
|
20
|
+
output="Working directory clean!"
|
|
21
|
+
if success
|
|
22
|
+
else "Uncommitted changes; clear before proceeding.",
|
|
23
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from potent.directives._base import BaseDirective, DirectiveResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GitPull(BaseDirective):
|
|
9
|
+
slug: Literal["git-pull"]
|
|
10
|
+
|
|
11
|
+
def _run(self, directory: Path) -> DirectiveResult:
|
|
12
|
+
result = self._run_cmd(directory, ["git", "pull"])
|
|
13
|
+
|
|
14
|
+
# print stdout? not sure if/when it's useful, can be long
|
|
15
|
+
return DirectiveResult.from_process(result)
|
|
16
|
+
|
|
17
|
+
if result.returncode == 0:
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
# print(result.stderr)
|
|
21
|
+
return False
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Literal, override
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from potent.directives._base import BaseDirective, DirectiveResult
|
|
8
|
+
from potent.util import truthy_list
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Config(BaseModel):
|
|
12
|
+
branch: str
|
|
13
|
+
create: bool = False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SwitchBranch(BaseDirective):
|
|
17
|
+
"""
|
|
18
|
+
Creates a branch if missing. Re-verifies that you're on that branch during every run.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
slug: Literal["switch-branch"]
|
|
22
|
+
config: Config
|
|
23
|
+
"""
|
|
24
|
+
branch name
|
|
25
|
+
"""
|
|
26
|
+
# base: Optional[str]
|
|
27
|
+
# """
|
|
28
|
+
# Branch to base the new branch off of. Will switch to this first to create the branch.
|
|
29
|
+
# """
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
def _run(self, directory: Path) -> DirectiveResult:
|
|
33
|
+
result = self._run_cmd(
|
|
34
|
+
directory,
|
|
35
|
+
["git", "switch", "-c" if self.config.create else "", self.config.branch],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return DirectiveResult.from_process(result)
|
|
39
|
+
|
|
40
|
+
if result.returncode == 0:
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
print(result.stderr)
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
# def verify(self) -> None:
|
|
47
|
+
# """
|
|
48
|
+
# verify branch on every run
|
|
49
|
+
# """
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated, Any, Literal, Optional, TextIO, Union
|
|
3
|
+
|
|
4
|
+
from annotated_types import Len
|
|
5
|
+
from pydantic import AfterValidator, BaseModel, Field
|
|
6
|
+
from rich.console import Group
|
|
7
|
+
from rich.tree import Tree
|
|
8
|
+
|
|
9
|
+
from potent.directives._base import AbsPath
|
|
10
|
+
from potent.directives.clean_workdir import CleanWorkdir
|
|
11
|
+
from potent.directives.git_pull import GitPull
|
|
12
|
+
from potent.directives.switch_branch import SwitchBranch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def unique_items(v):
|
|
16
|
+
if len(v) != len(set(v)):
|
|
17
|
+
raise ValueError("list is not unique")
|
|
18
|
+
return v
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Version = Literal["v1"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Shellprint(BaseModel):
|
|
25
|
+
version: Version = "v1"
|
|
26
|
+
comment: Optional[str] = None
|
|
27
|
+
steps: list[
|
|
28
|
+
Annotated[
|
|
29
|
+
Union[
|
|
30
|
+
GitPull,
|
|
31
|
+
SwitchBranch,
|
|
32
|
+
CleanWorkdir,
|
|
33
|
+
],
|
|
34
|
+
Field(discriminator="slug"),
|
|
35
|
+
]
|
|
36
|
+
]
|
|
37
|
+
directories: Annotated[
|
|
38
|
+
list[AbsPath],
|
|
39
|
+
Len(min_length=1),
|
|
40
|
+
AfterValidator(unique_items),
|
|
41
|
+
]
|
|
42
|
+
_path: Optional[Path] = None
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def from_file(f: TextIO) -> "Shellprint":
|
|
46
|
+
return Shellprint.model_validate_json(f.read())
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def from_path(f: Path) -> "Shellprint":
|
|
50
|
+
plan = Shellprint.model_validate_json(f.read_text())
|
|
51
|
+
plan._path = f
|
|
52
|
+
return plan
|
|
53
|
+
|
|
54
|
+
# def run(self):
|
|
55
|
+
# if not self._path:
|
|
56
|
+
# raise ValueError("Can't run plan without path")
|
|
57
|
+
|
|
58
|
+
# with self._path.open("r+") as fp:
|
|
59
|
+
# pass
|
|
60
|
+
|
|
61
|
+
def save(self, f: TextIO):
|
|
62
|
+
"""
|
|
63
|
+
Operates on an open file for performance reasons
|
|
64
|
+
"""
|
|
65
|
+
f.seek(0)
|
|
66
|
+
f.truncate()
|
|
67
|
+
f.write(self.model_dump_json(indent=2))
|
|
68
|
+
f.flush()
|
|
69
|
+
|
|
70
|
+
def reset(self):
|
|
71
|
+
for p in self.steps:
|
|
72
|
+
p.reset()
|
|
73
|
+
|
|
74
|
+
def render(self) -> Group:
|
|
75
|
+
"""
|
|
76
|
+
Outputs a task tree suitable for Rich to show progress (w/ spinners)
|
|
77
|
+
"""
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
def directory_complete(self, directory: Path) -> bool:
|
|
81
|
+
return all(s.completed(directory) for s in self.steps)
|
|
82
|
+
|
|
83
|
+
def directory_failed(self, directory: Path) -> bool:
|
|
84
|
+
return any(s.failed(directory) for s in self.steps)
|
|
85
|
+
|
|
86
|
+
# def directory_started(self, directory: Path) -> bool:
|
|
87
|
+
# return any(s.completed(directory) for s in self.steps)
|
|
88
|
+
|
|
89
|
+
def summarize(self, path: Path) -> Tree:
|
|
90
|
+
"""
|
|
91
|
+
Show this plan as plaintext
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
root = Tree(f"[yellow] {path.absolute()}")
|
|
95
|
+
# only print all steps if nothing has printed them yet
|
|
96
|
+
should_print_all = True
|
|
97
|
+
|
|
98
|
+
for d in self.directories:
|
|
99
|
+
# res.append("")
|
|
100
|
+
if self.directory_complete(d):
|
|
101
|
+
root.add(f"✅ {d.name}", style="green", guide_style="green")
|
|
102
|
+
|
|
103
|
+
# res.append(f"✅ {d.name}")
|
|
104
|
+
elif self.directory_failed(d):
|
|
105
|
+
should_print_all = False
|
|
106
|
+
failed = root.add(f"❌ {d.name}", style="red", guide_style="red")
|
|
107
|
+
# res.append(f"❌ {d.name}:\n")
|
|
108
|
+
for s in self.steps:
|
|
109
|
+
if s.completed(d):
|
|
110
|
+
failed.add(f"✅ {s.slug}", style="green")
|
|
111
|
+
# res.append(f" ✅ {s.slug}")
|
|
112
|
+
elif s.failed(d):
|
|
113
|
+
failed.add(f"❌ {s.slug}", style="red")
|
|
114
|
+
# res.append(f" ❌ {s.slug}")
|
|
115
|
+
else:
|
|
116
|
+
failed.add(f"⌛ {s.slug}", style="dim white")
|
|
117
|
+
# res.append(f" ⌛ {s.slug}")
|
|
118
|
+
else:
|
|
119
|
+
pending = root.add(f"⌛ {d.name}", style="yellow")
|
|
120
|
+
if should_print_all:
|
|
121
|
+
should_print_all = False
|
|
122
|
+
for s in self.steps:
|
|
123
|
+
pending.add(f"{s.slug}", style="white")
|
|
124
|
+
else:
|
|
125
|
+
pending.add("same as above", style="dim white")
|
|
126
|
+
|
|
127
|
+
# res.append(f"⌛ {d.name}")
|
|
128
|
+
|
|
129
|
+
# res.append("")
|
|
130
|
+
|
|
131
|
+
# return "\n".join(res)
|
|
132
|
+
return root
|