tasktree 0.0.3__py3-none-any.whl → 0.0.4__py3-none-any.whl

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.
tasktree/cli.py CHANGED
@@ -1,8 +1,5 @@
1
- """Command-line interface for Task Tree."""
2
-
3
1
  from __future__ import annotations
4
2
 
5
- import sys
6
3
  from pathlib import Path
7
4
  from typing import Any, List, Optional
8
5
 
@@ -120,7 +117,7 @@ def _init_recipe():
120
117
  raise typer.Exit(1)
121
118
 
122
119
  template = """# Task Tree Recipe
123
- # See https://github.com/kevinchannon/tasktree for documentation
120
+ # See https://github.com/kevinchannon/task-tree for documentation
124
121
 
125
122
  # Example task definitions:
126
123
 
@@ -168,22 +165,20 @@ def main(
168
165
  is_eager=True,
169
166
  help="Show version and exit",
170
167
  ),
171
- list_tasks: Optional[bool] = typer.Option(
172
- None, "--list", "-l", help="List all available tasks"
173
- ),
174
- show: Optional[str] = typer.Option(None, "--show", help="Show task definition"),
175
- tree: Optional[str] = typer.Option(None, "--tree", help="Show dependency tree"),
168
+ list_opt: Optional[bool] = typer.Option(None, "--list", "-l", help="List all available tasks"),
169
+ show: Optional[str] = typer.Option(None, "--show", "-s", help="Show task definition"),
170
+ tree: Optional[str] = typer.Option(None, "--tree", "-t", help="Show dependency tree"),
176
171
  init: Optional[bool] = typer.Option(
177
- None, "--init", help="Create a blank tasktree.yaml"
172
+ None, "--init", "-i", help="Create a blank tasktree.yaml"
178
173
  ),
179
174
  clean: Optional[bool] = typer.Option(
180
- None, "--clean", help="Remove state file (reset task cache)"
175
+ None, "--clean", "-c", help="Remove state file (reset task cache)"
181
176
  ),
182
177
  clean_state: Optional[bool] = typer.Option(
183
- None, "--clean-state", help="Remove state file (reset task cache)"
178
+ None, "--clean-state", "-C", help="Remove state file (reset task cache)"
184
179
  ),
185
180
  reset: Optional[bool] = typer.Option(
186
- None, "--reset", help="Remove state file (reset task cache)"
181
+ None, "--reset", "-r", help="Remove state file (reset task cache)"
187
182
  ),
188
183
  force: Optional[bool] = typer.Option(
189
184
  None, "--force", "-f", help="Force re-run all tasks (ignore freshness)"
@@ -210,38 +205,32 @@ def main(
210
205
  tt --list # List all tasks
211
206
  tt --tree test # Show dependency tree for 'test'
212
207
  """
213
- # Handle list option
214
- if list_tasks:
208
+
209
+ if list_opt:
215
210
  _list_tasks()
216
211
  raise typer.Exit()
217
212
 
218
- # Handle show option
219
213
  if show:
220
214
  _show_task(show)
221
215
  raise typer.Exit()
222
216
 
223
- # Handle tree option
224
217
  if tree:
225
218
  _show_tree(tree)
226
219
  raise typer.Exit()
227
220
 
228
- # Handle init option
229
221
  if init:
230
222
  _init_recipe()
231
223
  raise typer.Exit()
232
224
 
233
- # Handle clean options (all three aliases)
234
225
  if clean or clean_state or reset:
235
226
  _clean_state()
236
227
  raise typer.Exit()
237
228
 
238
- # Handle task execution
239
229
  if task_args:
240
- # When --only is specified, force execution (--only implies --force)
230
+ # --only implies --force
241
231
  force_execution = force or only or False
242
232
  _execute_dynamic_task(task_args, force=force_execution, only=only or False, env=env)
243
233
  else:
244
- # No arguments - show available tasks
245
234
  recipe = _get_recipe()
246
235
  if recipe is None:
247
236
  console.print("[red]No recipe file found (tasktree.yaml or tt.yaml)[/red]")
@@ -274,7 +263,7 @@ def _clean_state() -> None:
274
263
  console.print(f"[yellow]No state file found at {state_path}[/yellow]")
275
264
 
276
265
 
277
- def _get_recipe() -> Recipe | None:
266
+ def _get_recipe() -> Optional[Recipe]:
278
267
  """Get parsed recipe or None if not found."""
279
268
  recipe_path = find_recipe_file()
280
269
  if recipe_path is None:
@@ -287,15 +276,7 @@ def _get_recipe() -> Recipe | None:
287
276
  raise typer.Exit(1)
288
277
 
289
278
 
290
- def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = False, env: str | None = None) -> None:
291
- """Execute a task specified by name with arguments.
292
-
293
- Args:
294
- args: Command line arguments (task name and task arguments)
295
- force: If True, ignore freshness and re-run all tasks
296
- only: If True, run only the specified task without dependencies
297
- env: If provided, override environment for all tasks
298
- """
279
+ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = False, env: Optional[str] = None) -> None:
299
280
  if not args:
300
281
  return
301
282
 
@@ -350,31 +331,17 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
350
331
 
351
332
 
352
333
  def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, Any]:
353
- """Parse command line arguments for a task.
354
-
355
- Args:
356
- arg_specs: Argument specifications from task definition
357
- arg_values: Command line argument values
358
-
359
- Returns:
360
- Dictionary of argument names to values
361
-
362
- Raises:
363
- typer.Exit: If arguments are invalid
364
- """
365
334
  if not arg_specs:
366
335
  if arg_values:
367
336
  console.print(f"[red]Task does not accept arguments[/red]")
368
337
  raise typer.Exit(1)
369
338
  return {}
370
339
 
371
- # Parse argument specifications
372
340
  parsed_specs = []
373
341
  for spec in arg_specs:
374
342
  name, arg_type, default = parse_arg_spec(spec)
375
343
  parsed_specs.append((name, arg_type, default))
376
344
 
377
- # Build argument dictionary
378
345
  args_dict = {}
379
346
  positional_index = 0
380
347
 
@@ -424,14 +391,6 @@ def _parse_task_args(arg_specs: list[str], arg_values: list[str]) -> dict[str, A
424
391
 
425
392
 
426
393
  def _build_rich_tree(dep_tree: dict) -> Tree:
427
- """Build a Rich Tree from dependency tree structure.
428
-
429
- Args:
430
- dep_tree: Dependency tree structure
431
-
432
- Returns:
433
- Rich Tree for display
434
- """
435
394
  task_name = dep_tree["name"]
436
395
  tree = Tree(task_name)
437
396
 
tasktree/hasher.py CHANGED
@@ -1,77 +1,27 @@
1
- """Hashing logic for tasks and arguments."""
2
-
3
1
  import hashlib
4
2
  import json
5
- from typing import Any
3
+ from typing import Any, Optional
6
4
 
7
5
 
8
6
  def hash_task(cmd: str, outputs: list[str], working_dir: str, args: list[str], env: str = "") -> str:
9
- """Compute task definition hash.
10
-
11
- The hash includes:
12
- - cmd: The command to execute
13
- - outputs: Declared output files
14
- - working_dir: Execution directory
15
- - args: Parameter definitions (names and types)
16
- - env: Environment name (effective environment after resolution)
17
-
18
- The hash excludes:
19
- - deps: Only affects scheduling order
20
- - inputs: Tracked separately via timestamps
21
- - desc: Documentation only
22
-
23
- Args:
24
- cmd: Command to execute
25
- outputs: List of output glob patterns
26
- working_dir: Working directory for execution
27
- args: List of argument definitions
28
- env: Environment name (empty string for platform default)
29
-
30
- Returns:
31
- 8-character hex hash string
32
- """
33
- # Create a stable representation
34
7
  data = {
35
8
  "cmd": cmd,
36
- "outputs": sorted(outputs), # Sort for stability
9
+ "outputs": sorted(outputs),
37
10
  "working_dir": working_dir,
38
- "args": sorted(args), # Sort for stability
39
- "env": env, # Include effective environment
11
+ "args": sorted(args),
12
+ "env": env,
40
13
  }
41
14
 
42
- # Serialize to JSON with sorted keys for deterministic hashing
43
15
  serialized = json.dumps(data, sort_keys=True, separators=(",", ":"))
44
-
45
- # Compute hash and truncate to 8 characters
46
16
  return hashlib.sha256(serialized.encode()).hexdigest()[:8]
47
17
 
48
18
 
49
19
  def hash_args(args_dict: dict[str, Any]) -> str:
50
- """Compute hash of task arguments.
51
-
52
- Args:
53
- args_dict: Dictionary of argument names to values
54
-
55
- Returns:
56
- 8-character hex hash string
57
- """
58
- # Serialize arguments to JSON with sorted keys for deterministic hashing
59
20
  serialized = json.dumps(args_dict, sort_keys=True, separators=(",", ":"))
60
-
61
- # Compute hash and truncate to 8 characters
62
21
  return hashlib.sha256(serialized.encode()).hexdigest()[:8]
63
22
 
64
23
 
65
- def make_cache_key(task_hash: str, args_hash: str | None = None) -> str:
66
- """Create cache key for task execution.
67
-
68
- Args:
69
- task_hash: Task definition hash
70
- args_hash: Optional arguments hash
71
-
72
- Returns:
73
- Cache key string (task_hash or task_hash__args_hash)
74
- """
24
+ def make_cache_key(task_hash: str, args_hash: Optional[str] = None) -> str:
75
25
  if args_hash:
76
26
  return f"{task_hash}__{args_hash}"
77
27
  return task_hash
tasktree/types.py CHANGED
@@ -4,7 +4,7 @@ import re
4
4
  from datetime import datetime
5
5
  from ipaddress import IPv4Address, IPv6Address, ip_address
6
6
  from pathlib import Path
7
- from typing import Any
7
+ from typing import Any, Optional
8
8
 
9
9
  import click
10
10
 
@@ -19,7 +19,7 @@ class HostnameType(click.ParamType):
19
19
  r"^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*\.?$"
20
20
  )
21
21
 
22
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
22
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
23
23
  if isinstance(value, str):
24
24
  if self.HOSTNAME_PATTERN.match(value):
25
25
  return value
@@ -36,7 +36,7 @@ class EmailType(click.ParamType):
36
36
  r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
37
37
  )
38
38
 
39
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
39
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
40
40
  if isinstance(value, str):
41
41
  if self.EMAIL_PATTERN.match(value):
42
42
  return value
@@ -48,7 +48,7 @@ class IPType(click.ParamType):
48
48
 
49
49
  name = "ip"
50
50
 
51
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
51
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
52
52
  try:
53
53
  ip_address(value)
54
54
  return str(value)
@@ -61,7 +61,7 @@ class IPv4Type(click.ParamType):
61
61
 
62
62
  name = "ipv4"
63
63
 
64
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
64
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
65
65
  try:
66
66
  IPv4Address(value)
67
67
  return str(value)
@@ -74,7 +74,7 @@ class IPv6Type(click.ParamType):
74
74
 
75
75
  name = "ipv6"
76
76
 
77
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
77
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
78
78
  try:
79
79
  IPv6Address(value)
80
80
  return str(value)
@@ -87,7 +87,7 @@ class DateTimeType(click.ParamType):
87
87
 
88
88
  name = "datetime"
89
89
 
90
- def convert(self, value: Any, param: click.Parameter | None, ctx: click.Context | None) -> str:
90
+ def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
91
91
  if isinstance(value, str):
92
92
  try:
93
93
  datetime.fromisoformat(value)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -18,6 +18,95 @@ Description-Content-Type: text/markdown
18
18
 
19
19
  A task automation tool that combines simple command execution with dependency tracking and incremental execution.
20
20
 
21
+ ## Motivation
22
+ In any project of even moderate size, various scripts inevitably come into being along the way. These scripts often must be run in a particular order, or at a particular time. For historical reasons, this almost certainly a problem if your project is developed in a Linux environment; in Windows, an IDE like Visual Studio may be taking care of a significant proportion of your build, packaging and deployment tasks. Then again, it may not...
23
+
24
+ The various incantations that have to be issued to build, package, test and deploy a project can build up and then all of a sudden there's only a few people that remember which to invoke and when and then people start making helpful readme guides on what to do with the scripts and then those become out of date and start telling lies about things and so on.
25
+
26
+ Then there's the scripts themselves. In Linux, they're probably a big pile of Bash and Python, or something (Ruby, Perl, you name it). You can bet the house on people solving the problem of passing parameters to their scripts in a whole bunch of different and inconsistent ways.
27
+
28
+ ```bash
29
+ #!/usr/bin/env bash
30
+ # It's an environment variable defined.... somewhere?
31
+ echo "FOO is: $FOO"
32
+ ```
33
+ ```bash
34
+ #!/usr/bin/env bash
35
+ # Using simple positional arguments... guess what means what when you're invoking it!
36
+ echo "First: $1, Second: $2"
37
+ ```
38
+ ```bash
39
+ #!/usr/bin/env bash
40
+ # Oooooh fancy "make me look like a proper app" named option parsing... don't try and do --foo=bar though!
41
+ FOO=""
42
+ while [[ $# -gt 0 ]]; do
43
+ case "$1" in
44
+ --foo) FOO=$2; shift ;;
45
+ --) break ;;
46
+ *) echo "Unknown: $1";;
47
+ esac
48
+ shift
49
+ done
50
+ ```
51
+ ```bash
52
+ #!/usr/bin/env bash
53
+ # This thing...
54
+ ARGS=$(getopt -o f:b --long foo:,bar: -n 'myscript' -- "$@")
55
+ eval set -- "$ARGS"
56
+ while true; do
57
+ case "$1" in
58
+ -b|--bar) echo "Bar: $2"; shift 2 ;;
59
+ -f|--foo) echo "Foo: $2"; shift 2 ;;
60
+ --) shift; break ;;
61
+ *) break ;;
62
+ esac
63
+ done
64
+ ```
65
+
66
+ What about help info? Who has time to wire that in?
67
+
68
+ ### The point
69
+ Is this just whining and moaning? Should we just man up and revel in our own ability to memorize all the right incantations like some kind of scripting shaman?
70
+
71
+ ... No. That's **a dumb idea**.
72
+
73
+ Task Tree allows you to pile all the knowledge of **what** to run, **when** to run it, **where** to run it and **how** to run it into a single, readable place. Then you can delete all the scripts that no-one knows how to use and all the readme docs that lie to the few people that actually waste their time reading them.
74
+
75
+ The tasks you need to perform to deliver your project become summarised in an executable file that looks like:
76
+ ```yaml
77
+ build:
78
+ desc: Compile stuff
79
+ outputs: [target/release/bin]
80
+ cmd: cargo build --release
81
+
82
+ package:
83
+ desc: build installers
84
+ deps: [build]
85
+ outputs: [awesome.deb]
86
+ cmd: |
87
+ for bin in target/release/*; do
88
+ if [[ -x "$bin" && ! -d "$bin" ]]; then
89
+ install -Dm755 "$bin" "debian/awesome/usr/bin/$(basename "$bin")"
90
+ fi
91
+ done
92
+
93
+ dpkg-buildpackage -us -uc
94
+
95
+ test:
96
+ desc: Run tests
97
+ deps: [package]
98
+ inputs: [tests/**/*.py]
99
+ cmd: PYTHONPATH=src python3 -m pytest tests/ -v
100
+ ```
101
+
102
+ If you want to run the tests then:
103
+ ```bash
104
+ tt test
105
+ ```
106
+ Boom! Done. `build` will always run, because there's no sensible way to know what Cargo did. However, if Cargo decided that nothing needed to be done and didn't touch the binaries, then `package` will realize that and not do anything. Then `test` will just run with the new tests that you just wrote. If you then immediately run `test` again, then `test` will figure out that none of the dependencies did anything and that none of the test files have changed and then just _do nothing_ - as it should.
107
+
108
+ This is a toy example, but you can image how it plays out on a more complex project.
109
+
21
110
  ## Installation
22
111
 
23
112
  ### From PyPI (Recommended)
@@ -26,6 +115,16 @@ A task automation tool that combines simple command execution with dependency tr
26
115
  pipx install tasktree
27
116
  ```
28
117
 
118
+ If you have multiple Python interpreter versions installed, and the _default_ interpreter is a version <3.11, then you can use `pipx`'s `--python` option to specify an interpreter with a version >=3.11:
119
+
120
+ ```bash
121
+ # If the target version is on the PATH
122
+ pipx install --python python3.12 tasktree
123
+
124
+ # With a path to an interpreter
125
+ pipx install --python /path/to/python3.12 tasktree
126
+ ```
127
+
29
128
  ### From Source
30
129
 
31
130
  For the latest unreleased version from GitHub:
@@ -61,9 +160,11 @@ test:
61
160
  Run tasks:
62
161
 
63
162
  ```bash
64
- tt build # Build the application
65
- tt test # Run tests (builds first if needed)
163
+ tt # Print the help
164
+ tt --help # ...also print the help
66
165
  tt --list # Show all available tasks
166
+ tt build # Build the application (assuming this is in your tasktree.yaml)
167
+ tt test # Run tests (builds first if needed)
67
168
  ```
68
169
 
69
170
  ## Core Concepts
@@ -1,13 +1,13 @@
1
1
  tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
2
- tasktree/cli.py,sha256=hfxKF4N8nKPvy3WwyQgENHNH8YbUD3Zd7DLHGuHdeHU,14400
2
+ tasktree/cli.py,sha256=2Pm0pxWdG4RaQbVMijaMUIrm3v0b9J98CUsUp7cDFvI,13236
3
3
  tasktree/executor.py,sha256=_E37tShHuiOj0Mvx2GbS9y3GIozC3hpzAVhAjbvYJqg,18638
4
4
  tasktree/graph.py,sha256=9ngfg93y7EkOIN_lUQa0u-JhnwiMN1UdQQvIFw8RYCE,4181
5
- tasktree/hasher.py,sha256=xOeth3vufP-QrjnVgTDh02clkZb867aJPD6HSjhMNsg,2336
5
+ tasktree/hasher.py,sha256=puJey9wF_p37k_xqjhYr_6ICsbAfrTBWHec6MqKV4BU,814
6
6
  tasktree/parser.py,sha256=apLfN3_YVa7lQIy0rcHFwo931Pg-8mKG1044AQE6fLQ,12690
7
7
  tasktree/state.py,sha256=rxKtS3SbsPtAuraHbN807RGWfoYYkQ3pe8CxUstwo2k,3535
8
8
  tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
9
- tasktree/types.py,sha256=wrBzO-Z2ebCTRjWyOWNvuCjqAq-74Zyb9E4FQ4beF38,3751
10
- tasktree-0.0.3.dist-info/METADATA,sha256=qlpV-egfSY502xJKLmmam1CTUVpVEiU4oS10LntL7gE,11961
11
- tasktree-0.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- tasktree-0.0.3.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
13
- tasktree-0.0.3.dist-info/RECORD,,
9
+ tasktree/types.py,sha256=w--sKjRTc8mGYkU5eAduqV86SolDqOYspAPuVKIuSQQ,3797
10
+ tasktree-0.0.4.dist-info/METADATA,sha256=dxJ5QiaRWhBbae-cK358ESQuwbjDg04ITWomlgrO9Lc,16312
11
+ tasktree-0.0.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ tasktree-0.0.4.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
13
+ tasktree-0.0.4.dist-info/RECORD,,