cook-build 0.5.1__tar.gz → 0.6.1__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.
- cook_build-0.6.1/PKG-INFO +91 -0
- cook_build-0.6.1/README.md +81 -0
- {cook-build-0.5.1 → cook_build-0.6.1}/README.rst +6 -6
- cook_build-0.6.1/pyproject.toml +25 -0
- cook_build-0.6.1/setup.cfg +4 -0
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/__main__.py +146 -63
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/actions.py +21 -13
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/contexts.py +33 -14
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/controller.py +157 -56
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/manager.py +41 -22
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/task.py +13 -13
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/util.py +14 -9
- cook_build-0.6.1/src/cook_build.egg-info/PKG-INFO +91 -0
- cook_build-0.6.1/src/cook_build.egg-info/SOURCES.txt +25 -0
- cook_build-0.6.1/src/cook_build.egg-info/requires.txt +2 -0
- {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_contexts.py +45 -16
- {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_controller.py +109 -37
- {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_examples.py +6 -3
- {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_main.py +27 -17
- {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_manager.py +16 -7
- {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_util.py +7 -2
- cook-build-0.5.1/PKG-INFO +0 -91
- cook-build-0.5.1/cook_build.egg-info/PKG-INFO +0 -91
- cook-build-0.5.1/cook_build.egg-info/SOURCES.txt +0 -25
- cook-build-0.5.1/cook_build.egg-info/requires.txt +0 -2
- cook-build-0.5.1/setup.cfg +0 -18
- cook-build-0.5.1/setup.py +0 -31
- {cook-build-0.5.1 → cook_build-0.6.1}/LICENSE +0 -0
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook/__init__.py +0 -0
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook_build.egg-info/dependency_links.txt +0 -0
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook_build.egg-info/entry_points.txt +0 -0
- {cook-build-0.5.1 → cook_build-0.6.1/src}/cook_build.egg-info/top_level.txt +0 -0
- {cook-build-0.5.1 → cook_build-0.6.1}/tests/test_actions.py +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cook-build
|
|
3
|
+
Version: 0.6.1
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Dist: colorama>=0.4.6
|
|
8
|
+
Requires-Dist: networkx>=3.2.1
|
|
9
|
+
Dynamic: license-file
|
|
10
|
+
|
|
11
|
+
# 🧑🍳 Cook [](https://github.com/tillahoffmann/cook-build/actions/workflows/main.yaml) [](https://pypi.org/project/cook-build)
|
|
12
|
+
|
|
13
|
+
Cook is a task-centric build system with simple declarative recipes specified in Python.
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
Tasks are declared in a `recipe.py` file using the `cook.manager.create_task` function. Each task must have a unique name, may depend on files or other tasks, and can execute an action, typically a shell command. The simple example below creates a C source file, compiles it, and executes the binary.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
>>> from cook import create_task
|
|
21
|
+
|
|
22
|
+
>>> create_task("src", targets=["hello.c"],
|
|
23
|
+
... action="echo 'int main() { return 0; }' > hello.c")
|
|
24
|
+
>>> create_task("cc", dependencies=["hello.c"], targets=["hello"],
|
|
25
|
+
... action="cc -o hello hello.c")
|
|
26
|
+
>>> create_task("hello", dependencies=["hello"], action="./hello")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Running `cook ls` from the command line lists all known tasks, e.g.,
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
$ cook ls
|
|
33
|
+
<task `src` @ /.../recipe.py:3>
|
|
34
|
+
<task `cc` @ /.../recipe.py:6>
|
|
35
|
+
<task `hello` @ /.../recipe.py:9>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Running `cook exec hello` creates the source file, compiles it, and executes the binary (using `--log-level=debug` can provide additional information).
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
$ cook exec hello
|
|
42
|
+
INFO: executing <task `src` @ /.../recipe.py:3> ...
|
|
43
|
+
INFO: completed <task `src` @ /.../recipe.py:3> in 0:00:...
|
|
44
|
+
INFO: executing <task `cc` @ /.../recipe.py:6> ...
|
|
45
|
+
INFO: completed <task `cc` @ /.../recipe.py:6> in 0:00:...
|
|
46
|
+
INFO: executing <task `hello` @ /.../recipe.py:9> ...
|
|
47
|
+
INFO: completed <task `hello` @ /.../recipe.py:9> in 0:00:...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To rerun a task, tell Cook to reset it.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
$ cook reset cc
|
|
54
|
+
INFO: reset 1 task
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The full set of available commands can be explored using `cook --help` as shown below.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
$ cook --help
|
|
61
|
+
usage: cook [-h] [--recipe RECIPE] [--module MODULE] [--db DB]
|
|
62
|
+
[--log-level {warning,error,info,debug}]
|
|
63
|
+
{exec,ls,info,reset} ...
|
|
64
|
+
|
|
65
|
+
positional arguments:
|
|
66
|
+
{exec,ls,info,reset}
|
|
67
|
+
exec Execute one or more tasks.
|
|
68
|
+
ls List tasks.
|
|
69
|
+
info Display information about one or more tasks.
|
|
70
|
+
reset Reset the status of one or more tasks.
|
|
71
|
+
|
|
72
|
+
options:
|
|
73
|
+
-h, --help show this help message and exit
|
|
74
|
+
--recipe RECIPE file containing declarative recipe for tasks
|
|
75
|
+
--module, -m MODULE module containing declarative recipe for tasks
|
|
76
|
+
--db DB database for keeping track of assets
|
|
77
|
+
--log-level {warning,error,info,debug}
|
|
78
|
+
log level
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tasks Are Dumb; Contexts Are Smart
|
|
82
|
+
|
|
83
|
+
`cook.task.Task`s do not provide any functionality beyond storing metadata, including
|
|
84
|
+
|
|
85
|
+
- `targets`, the files generated by the task,
|
|
86
|
+
- `dependencies`, the files the task depends on,
|
|
87
|
+
- `action`, the `cook.actions.Action` to execute when the task is run,
|
|
88
|
+
- `task_dependencies`, other tasks that should be executed first,
|
|
89
|
+
- `location`, filename and line number where the task was defined.
|
|
90
|
+
|
|
91
|
+
All logic is handled by `cook.contexts.Context`s which are applied to each task when it is created. For example, `cook.contexts.create_group` adds all tasks created within the context to a group. This group can be executed to execute all child tasks.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# 🧑🍳 Cook [](https://github.com/tillahoffmann/cook-build/actions/workflows/main.yaml) [](https://pypi.org/project/cook-build)
|
|
2
|
+
|
|
3
|
+
Cook is a task-centric build system with simple declarative recipes specified in Python.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
Tasks are declared in a `recipe.py` file using the `cook.manager.create_task` function. Each task must have a unique name, may depend on files or other tasks, and can execute an action, typically a shell command. The simple example below creates a C source file, compiles it, and executes the binary.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
>>> from cook import create_task
|
|
11
|
+
|
|
12
|
+
>>> create_task("src", targets=["hello.c"],
|
|
13
|
+
... action="echo 'int main() { return 0; }' > hello.c")
|
|
14
|
+
>>> create_task("cc", dependencies=["hello.c"], targets=["hello"],
|
|
15
|
+
... action="cc -o hello hello.c")
|
|
16
|
+
>>> create_task("hello", dependencies=["hello"], action="./hello")
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Running `cook ls` from the command line lists all known tasks, e.g.,
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
$ cook ls
|
|
23
|
+
<task `src` @ /.../recipe.py:3>
|
|
24
|
+
<task `cc` @ /.../recipe.py:6>
|
|
25
|
+
<task `hello` @ /.../recipe.py:9>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Running `cook exec hello` creates the source file, compiles it, and executes the binary (using `--log-level=debug` can provide additional information).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ cook exec hello
|
|
32
|
+
INFO: executing <task `src` @ /.../recipe.py:3> ...
|
|
33
|
+
INFO: completed <task `src` @ /.../recipe.py:3> in 0:00:...
|
|
34
|
+
INFO: executing <task `cc` @ /.../recipe.py:6> ...
|
|
35
|
+
INFO: completed <task `cc` @ /.../recipe.py:6> in 0:00:...
|
|
36
|
+
INFO: executing <task `hello` @ /.../recipe.py:9> ...
|
|
37
|
+
INFO: completed <task `hello` @ /.../recipe.py:9> in 0:00:...
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To rerun a task, tell Cook to reset it.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
$ cook reset cc
|
|
44
|
+
INFO: reset 1 task
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The full set of available commands can be explored using `cook --help` as shown below.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
$ cook --help
|
|
51
|
+
usage: cook [-h] [--recipe RECIPE] [--module MODULE] [--db DB]
|
|
52
|
+
[--log-level {warning,error,info,debug}]
|
|
53
|
+
{exec,ls,info,reset} ...
|
|
54
|
+
|
|
55
|
+
positional arguments:
|
|
56
|
+
{exec,ls,info,reset}
|
|
57
|
+
exec Execute one or more tasks.
|
|
58
|
+
ls List tasks.
|
|
59
|
+
info Display information about one or more tasks.
|
|
60
|
+
reset Reset the status of one or more tasks.
|
|
61
|
+
|
|
62
|
+
options:
|
|
63
|
+
-h, --help show this help message and exit
|
|
64
|
+
--recipe RECIPE file containing declarative recipe for tasks
|
|
65
|
+
--module, -m MODULE module containing declarative recipe for tasks
|
|
66
|
+
--db DB database for keeping track of assets
|
|
67
|
+
--log-level {warning,error,info,debug}
|
|
68
|
+
log level
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Tasks Are Dumb; Contexts Are Smart
|
|
72
|
+
|
|
73
|
+
`cook.task.Task`s do not provide any functionality beyond storing metadata, including
|
|
74
|
+
|
|
75
|
+
- `targets`, the files generated by the task,
|
|
76
|
+
- `dependencies`, the files the task depends on,
|
|
77
|
+
- `action`, the `cook.actions.Action` to execute when the task is run,
|
|
78
|
+
- `task_dependencies`, other tasks that should be executed first,
|
|
79
|
+
- `location`, filename and line number where the task was defined.
|
|
80
|
+
|
|
81
|
+
All logic is handled by `cook.contexts.Context`s which are applied to each task when it is created. For example, `cook.contexts.create_group` adds all tasks created within the context to a group. This group can be executed to execute all child tasks.
|
|
@@ -30,8 +30,8 @@ Running :code:`cook ls` from the command line lists all known tasks, e.g.,
|
|
|
30
30
|
|
|
31
31
|
$ cook ls
|
|
32
32
|
<task `src` @ /.../recipe.py:3>
|
|
33
|
-
<task `cc` @ /.../recipe.py:
|
|
34
|
-
<task `hello` @ /.../recipe.py:
|
|
33
|
+
<task `cc` @ /.../recipe.py:6>
|
|
34
|
+
<task `hello` @ /.../recipe.py:9>
|
|
35
35
|
|
|
36
36
|
Running :code:`cook exec hello` creates the source file, compiles it, and executes the binary (using :code:`--log-level=debug` can provide additional information).
|
|
37
37
|
|
|
@@ -42,10 +42,10 @@ Running :code:`cook exec hello` creates the source file, compiles it, and execut
|
|
|
42
42
|
$ cook exec hello
|
|
43
43
|
INFO: executing <task `src` @ /.../recipe.py:3> ...
|
|
44
44
|
INFO: completed <task `src` @ /.../recipe.py:3> in 0:00:...
|
|
45
|
-
INFO: executing <task `cc` @ /.../recipe.py:
|
|
46
|
-
INFO: completed <task `cc` @ /.../recipe.py:
|
|
47
|
-
INFO: executing <task `hello` @ /.../recipe.py:
|
|
48
|
-
INFO: completed <task `hello` @ /.../recipe.py:
|
|
45
|
+
INFO: executing <task `cc` @ /.../recipe.py:6> ...
|
|
46
|
+
INFO: completed <task `cc` @ /.../recipe.py:6> in 0:00:...
|
|
47
|
+
INFO: executing <task `hello` @ /.../recipe.py:9> ...
|
|
48
|
+
INFO: completed <task `hello` @ /.../recipe.py:9> in 0:00:...
|
|
49
49
|
|
|
50
50
|
To rerun a task, tell Cook to reset it.
|
|
51
51
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cook-build"
|
|
3
|
+
version = "0.6.1"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
dependencies = [
|
|
6
|
+
"colorama>=0.4.6",
|
|
7
|
+
"networkx>=3.2.1",
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
|
|
11
|
+
[dependency-groups]
|
|
12
|
+
dev = [
|
|
13
|
+
"build>=1.3.0",
|
|
14
|
+
"furo>=2025.9.25",
|
|
15
|
+
"pyright>=1.1.406",
|
|
16
|
+
"pytest>=8.4.2",
|
|
17
|
+
"pytest-cov>=7.0.0",
|
|
18
|
+
"ruff>=0.13.3",
|
|
19
|
+
"sphinx>=7.4.7",
|
|
20
|
+
"sphinxcontrib-shtest>=0.5.0",
|
|
21
|
+
"twine>=6.2.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
cook = "cook.__main__:__main__"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import colorama
|
|
3
|
+
from contextlib import closing
|
|
3
4
|
from datetime import datetime
|
|
4
5
|
import fnmatch
|
|
5
6
|
import importlib.util
|
|
@@ -10,9 +11,13 @@ import re
|
|
|
10
11
|
import sqlite3
|
|
11
12
|
import sys
|
|
12
13
|
import textwrap
|
|
13
|
-
from typing import Iterable
|
|
14
|
+
from typing import Iterable
|
|
14
15
|
|
|
15
|
-
from .contexts import
|
|
16
|
+
from .contexts import (
|
|
17
|
+
create_target_directories,
|
|
18
|
+
normalize_action,
|
|
19
|
+
normalize_dependencies,
|
|
20
|
+
)
|
|
16
21
|
from .controller import Controller, QUERIES
|
|
17
22
|
from .manager import Manager
|
|
18
23
|
from .task import Task
|
|
@@ -23,22 +28,28 @@ LOGGER = logging.getLogger("cook")
|
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
class NoMatchingTaskError(ValueError):
|
|
26
|
-
def __init__(
|
|
31
|
+
def __init__(
|
|
32
|
+
self, patterns: Iterable[re.Pattern | str], hidden_tasks_available: bool
|
|
33
|
+
) -> None:
|
|
27
34
|
self.hidden_tasks_available = hidden_tasks_available
|
|
28
|
-
|
|
29
|
-
if len(
|
|
30
|
-
message = f"found no tasks matching pattern {
|
|
35
|
+
formatted_patterns = [f"`{pattern}`" for pattern in patterns]
|
|
36
|
+
if len(formatted_patterns) == 1:
|
|
37
|
+
message = f"found no tasks matching pattern {formatted_patterns[0]}"
|
|
31
38
|
else:
|
|
32
|
-
*
|
|
33
|
-
message =
|
|
34
|
-
|
|
39
|
+
*formatted_patterns, last = formatted_patterns
|
|
40
|
+
message = (
|
|
41
|
+
"found no tasks matching patterns "
|
|
42
|
+
+ ", ".join(formatted_patterns)
|
|
43
|
+
+ (", or " if len(formatted_patterns) > 1 else " or ")
|
|
44
|
+
+ last
|
|
45
|
+
)
|
|
35
46
|
if hidden_tasks_available:
|
|
36
47
|
message = message + "; use --all or -a to include hidden tasks"
|
|
37
48
|
super().__init__(message)
|
|
38
49
|
|
|
39
50
|
|
|
40
51
|
class Args:
|
|
41
|
-
tasks: Iterable[re.Pattern]
|
|
52
|
+
tasks: Iterable[re.Pattern | str]
|
|
42
53
|
re: bool
|
|
43
54
|
all: bool
|
|
44
55
|
|
|
@@ -47,23 +58,35 @@ class Command:
|
|
|
47
58
|
"""
|
|
48
59
|
Abstract base class for commands.
|
|
49
60
|
"""
|
|
50
|
-
|
|
61
|
+
|
|
62
|
+
NAME: str | None = None
|
|
51
63
|
ALLOW_EMPTY_PATTERN: bool = False
|
|
52
64
|
|
|
53
65
|
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
54
|
-
parser.add_argument(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--re",
|
|
68
|
+
"-r",
|
|
69
|
+
action="store_true",
|
|
70
|
+
help="use regular expressions for pattern matching instead of glob",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--all",
|
|
74
|
+
"-a",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="include tasks starting with `_` prefix",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"tasks",
|
|
80
|
+
nargs="*" if self.ALLOW_EMPTY_PATTERN else "+",
|
|
81
|
+
help="task or tasks to execute as regular expressions or glob patterns",
|
|
82
|
+
)
|
|
60
83
|
|
|
61
84
|
def execute(self, controller: Controller, args: argparse.Namespace) -> None:
|
|
62
85
|
raise NotImplementedError
|
|
63
86
|
|
|
64
|
-
def discover_tasks(self, controller: Controller, args: Args) ->
|
|
87
|
+
def discover_tasks(self, controller: Controller, args: Args) -> list[Task]:
|
|
65
88
|
task: Task
|
|
66
|
-
tasks:
|
|
89
|
+
tasks: list[Task] = []
|
|
67
90
|
|
|
68
91
|
# Get tasks based on the pattern matching.
|
|
69
92
|
if not args.tasks:
|
|
@@ -73,7 +96,10 @@ class Command:
|
|
|
73
96
|
if args.re:
|
|
74
97
|
match = any(re.match(pattern, task.name) for pattern in args.tasks)
|
|
75
98
|
else:
|
|
76
|
-
match = any(
|
|
99
|
+
match = any(
|
|
100
|
+
fnmatch.fnmatch(task.name, pattern) # pyright: ignore[reportArgumentType]
|
|
101
|
+
for pattern in args.tasks
|
|
102
|
+
)
|
|
77
103
|
if match:
|
|
78
104
|
tasks.append(task)
|
|
79
105
|
|
|
@@ -83,8 +109,11 @@ class Command:
|
|
|
83
109
|
# Filter out hidden tasks if desired unless the name is an exact match to a specified
|
|
84
110
|
# pattern.
|
|
85
111
|
if not args.all:
|
|
86
|
-
tasks = [
|
|
87
|
-
|
|
112
|
+
tasks = [
|
|
113
|
+
task
|
|
114
|
+
for task in tasks
|
|
115
|
+
if not task.name.startswith("_") or task.name in args.tasks
|
|
116
|
+
]
|
|
88
117
|
|
|
89
118
|
if not tasks:
|
|
90
119
|
raise NoMatchingTaskError(args.tasks, not args.all and has_hidden_task)
|
|
@@ -99,13 +128,16 @@ class ExecCommand(Command):
|
|
|
99
128
|
"""
|
|
100
129
|
Execute one or more tasks.
|
|
101
130
|
"""
|
|
131
|
+
|
|
102
132
|
NAME = "exec"
|
|
103
133
|
|
|
104
134
|
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
105
|
-
parser.add_argument(
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--jobs", "-j", help="number of concurrent jobs", type=int, default=1
|
|
137
|
+
)
|
|
106
138
|
super().configure_parser(parser)
|
|
107
139
|
|
|
108
|
-
def execute(self, controller: Controller, args: ExecArgs) -> None:
|
|
140
|
+
def execute(self, controller: Controller, args: ExecArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
109
141
|
tasks = self.discover_tasks(controller, args)
|
|
110
142
|
controller.execute(tasks, num_concurrent=args.jobs)
|
|
111
143
|
|
|
@@ -119,28 +151,43 @@ class LsCommand(Command):
|
|
|
119
151
|
"""
|
|
120
152
|
List tasks.
|
|
121
153
|
"""
|
|
154
|
+
|
|
122
155
|
NAME = "ls"
|
|
123
156
|
ALLOW_EMPTY_PATTERN = True
|
|
124
157
|
|
|
125
158
|
def configure_parser(self, parser: argparse.ArgumentParser) -> None:
|
|
126
|
-
parser.add_argument(
|
|
127
|
-
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--stale", "-s", action="store_true", help="only show stale tasks"
|
|
161
|
+
)
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--current", "-c", action="store_true", help="only show current tasks"
|
|
164
|
+
)
|
|
128
165
|
super().configure_parser(parser)
|
|
129
166
|
|
|
130
|
-
def execute(self, controller: Controller, args: LsArgs) -> None:
|
|
167
|
+
def execute(self, controller: Controller, args: LsArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
131
168
|
tasks = self.discover_tasks(controller, args)
|
|
132
|
-
tasks = [
|
|
133
|
-
|
|
169
|
+
tasks = [
|
|
170
|
+
task.format(colorama.Fore.YELLOW if is_stale else colorama.Fore.GREEN)
|
|
171
|
+
for is_stale, task in zip(controller.is_stale(tasks), tasks)
|
|
172
|
+
]
|
|
134
173
|
print("\n".join(tasks))
|
|
135
174
|
|
|
136
|
-
def discover_tasks(self, controller: Controller, args: LsArgs) ->
|
|
175
|
+
def discover_tasks(self, controller: Controller, args: LsArgs) -> list[Task]: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
137
176
|
if args.current and args.stale:
|
|
138
|
-
raise ValueError(
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"only one of `--stale` and `--current` may be given at the same time"
|
|
179
|
+
)
|
|
139
180
|
tasks = super().discover_tasks(controller, args)
|
|
140
181
|
if args.stale:
|
|
141
|
-
return [
|
|
182
|
+
return [
|
|
183
|
+
task for stale, task in zip(controller.is_stale(tasks), tasks) if stale
|
|
184
|
+
]
|
|
142
185
|
elif args.current:
|
|
143
|
-
return [
|
|
186
|
+
return [
|
|
187
|
+
task
|
|
188
|
+
for stale, task in zip(controller.is_stale(tasks), tasks)
|
|
189
|
+
if not stale
|
|
190
|
+
]
|
|
144
191
|
return tasks
|
|
145
192
|
|
|
146
193
|
|
|
@@ -148,6 +195,7 @@ class InfoCommand(LsCommand):
|
|
|
148
195
|
"""
|
|
149
196
|
Display information about one or more tasks.
|
|
150
197
|
"""
|
|
198
|
+
|
|
151
199
|
NAME = "info"
|
|
152
200
|
|
|
153
201
|
def execute(self, controller: Controller, args: LsArgs) -> None:
|
|
@@ -166,21 +214,26 @@ class InfoCommand(LsCommand):
|
|
|
166
214
|
]
|
|
167
215
|
# Show when the task last completed and failed.
|
|
168
216
|
last = controller.connection.execute(
|
|
169
|
-
"SELECT last_completed, last_failed FROM tasks WHERE name = :name",
|
|
170
|
-
{"name": task.name}
|
|
171
|
-
).fetchone() or (None, None)
|
|
172
|
-
for key, value in zip(["completed", "failed"], last):
|
|
217
|
+
"SELECT last_started, last_completed, last_failed FROM tasks WHERE name = :name",
|
|
218
|
+
{"name": task.name},
|
|
219
|
+
).fetchone() or (None, None, None)
|
|
220
|
+
for key, value in zip(["started", "completed", "failed"], last):
|
|
173
221
|
if value is None:
|
|
174
222
|
parts.append(f"last {key}: -")
|
|
175
223
|
continue
|
|
176
|
-
parts.append(
|
|
177
|
-
|
|
224
|
+
parts.append(
|
|
225
|
+
f"last {key}: {format_timedelta(datetime.now() - value)} ago "
|
|
226
|
+
f"({format_datetime(value)})"
|
|
227
|
+
)
|
|
178
228
|
# Show dependencies and targets.
|
|
179
|
-
task_dependencies = list(
|
|
180
|
-
|
|
229
|
+
task_dependencies = list(
|
|
230
|
+
sorted(controller.dependencies.successors(task), key=lambda t: t.name)
|
|
231
|
+
)
|
|
181
232
|
task_dependencies = [
|
|
182
233
|
dep.format(colorama.Fore.YELLOW if is_stale else colorama.Fore.GREEN)
|
|
183
|
-
for is_stale, dep in zip(
|
|
234
|
+
for is_stale, dep in zip(
|
|
235
|
+
controller.is_stale(task_dependencies), task_dependencies
|
|
236
|
+
)
|
|
184
237
|
]
|
|
185
238
|
items = [
|
|
186
239
|
("dependencies", task.dependencies),
|
|
@@ -195,22 +248,22 @@ class InfoCommand(LsCommand):
|
|
|
195
248
|
parts.append(f"{key}: -")
|
|
196
249
|
parts.append(f"action: {task.action if task.action else '-'}")
|
|
197
250
|
|
|
198
|
-
parts = textwrap.indent(
|
|
251
|
+
parts = textwrap.indent("\n".join(parts), indent)
|
|
199
252
|
print(f"{task}\n{parts}")
|
|
200
253
|
|
|
201
254
|
|
|
202
|
-
class ResetArgs(
|
|
203
|
-
|
|
204
|
-
re: bool
|
|
255
|
+
class ResetArgs(Args):
|
|
256
|
+
pass
|
|
205
257
|
|
|
206
258
|
|
|
207
259
|
class ResetCommand(Command):
|
|
208
260
|
"""
|
|
209
261
|
Reset the status of one or more tasks.
|
|
210
262
|
"""
|
|
263
|
+
|
|
211
264
|
NAME = "reset"
|
|
212
265
|
|
|
213
|
-
def execute(self, controller: Controller, args: ResetArgs) -> None:
|
|
266
|
+
def execute(self, controller: Controller, args: ResetArgs) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
214
267
|
tasks = self.discover_tasks(controller, args)
|
|
215
268
|
controller.reset(*tasks)
|
|
216
269
|
|
|
@@ -233,14 +286,26 @@ class Formatter(logging.Formatter):
|
|
|
233
286
|
return formatted
|
|
234
287
|
|
|
235
288
|
|
|
236
|
-
def __main__(cli_args:
|
|
289
|
+
def __main__(cli_args: list[str] | None = None) -> None:
|
|
237
290
|
parser = argparse.ArgumentParser("cook")
|
|
238
|
-
parser.add_argument(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
291
|
+
parser.add_argument(
|
|
292
|
+
"--recipe",
|
|
293
|
+
help="file containing declarative recipe for tasks",
|
|
294
|
+
default="recipe.py",
|
|
295
|
+
type=Path,
|
|
296
|
+
)
|
|
297
|
+
parser.add_argument(
|
|
298
|
+
"--module", "-m", help="module containing declarative recipe for tasks"
|
|
299
|
+
)
|
|
300
|
+
parser.add_argument(
|
|
301
|
+
"--db", help="database for keeping track of assets", default=".cook"
|
|
302
|
+
)
|
|
303
|
+
parser.add_argument(
|
|
304
|
+
"--log-level",
|
|
305
|
+
help="log level",
|
|
306
|
+
default="info",
|
|
307
|
+
choices={"error", "warning", "info", "debug"},
|
|
308
|
+
)
|
|
244
309
|
subparsers = parser.add_subparsers()
|
|
245
310
|
subparsers.required = True
|
|
246
311
|
|
|
@@ -258,11 +323,13 @@ def __main__(cli_args: Optional[List[str]] = None) -> None:
|
|
|
258
323
|
|
|
259
324
|
with Manager() as manager:
|
|
260
325
|
try:
|
|
261
|
-
manager.contexts.extend(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
326
|
+
manager.contexts.extend(
|
|
327
|
+
[
|
|
328
|
+
create_target_directories(),
|
|
329
|
+
normalize_action(),
|
|
330
|
+
normalize_dependencies(),
|
|
331
|
+
]
|
|
332
|
+
)
|
|
266
333
|
if args.module:
|
|
267
334
|
# Temporarily add the current working directory to the path.
|
|
268
335
|
try:
|
|
@@ -273,17 +340,23 @@ def __main__(cli_args: Optional[List[str]] = None) -> None:
|
|
|
273
340
|
elif args.recipe.is_file():
|
|
274
341
|
# Parse the recipe.
|
|
275
342
|
spec = importlib.util.spec_from_file_location("recipe", args.recipe)
|
|
343
|
+
assert spec, f"Could not load spec for '{args.recipe}'."
|
|
276
344
|
recipe = importlib.util.module_from_spec(spec)
|
|
345
|
+
assert spec.loader, f"Could not load recipe '{args.recipe}'."
|
|
277
346
|
spec.loader.exec_module(recipe)
|
|
278
347
|
else: # pragma: no cover
|
|
279
|
-
raise ValueError(
|
|
280
|
-
|
|
348
|
+
raise ValueError(
|
|
349
|
+
"recipe file or module must be specified; default recipe.py not "
|
|
350
|
+
"found"
|
|
351
|
+
)
|
|
281
352
|
except: # noqa: E722
|
|
282
353
|
LOGGER.fatal("failed to load recipe", exc_info=sys.exc_info())
|
|
283
354
|
sys.exit(1)
|
|
284
355
|
|
|
285
|
-
with
|
|
286
|
-
|
|
356
|
+
with closing(
|
|
357
|
+
sqlite3.connect(args.db, detect_types=sqlite3.PARSE_DECLTYPES)
|
|
358
|
+
) as connection:
|
|
359
|
+
_setup_schema(connection)
|
|
287
360
|
controller = Controller(manager.resolve_dependencies(), connection)
|
|
288
361
|
command: Command = args.command
|
|
289
362
|
try:
|
|
@@ -297,5 +370,15 @@ def __main__(cli_args: Optional[List[str]] = None) -> None:
|
|
|
297
370
|
sys.exit(1)
|
|
298
371
|
|
|
299
372
|
|
|
373
|
+
def _setup_schema(connection: sqlite3.Connection) -> None:
|
|
374
|
+
connection.executescript(QUERIES["schema"])
|
|
375
|
+
# Attempt to add the column which may not be present for cook versions <0.6.
|
|
376
|
+
try:
|
|
377
|
+
connection.execute("ALTER TABLE tasks ADD COLUMN last_started TIMESTAMP")
|
|
378
|
+
except sqlite3.OperationalError as ex:
|
|
379
|
+
if "duplicate column name" not in ex.args[0]:
|
|
380
|
+
raise # pragma: no cover
|
|
381
|
+
|
|
382
|
+
|
|
300
383
|
if __name__ == "__main__":
|
|
301
384
|
__main__()
|