tasktree 0.0.12__py3-none-any.whl → 0.0.14__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,5 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
4
+ import sys
3
5
  from pathlib import Path
4
6
  from typing import Any, List, Optional
5
7
 
@@ -26,6 +28,46 @@ app = typer.Typer(
26
28
  console = Console()
27
29
 
28
30
 
31
+ def _supports_unicode() -> bool:
32
+ """Check if the terminal supports Unicode characters.
33
+
34
+ Returns:
35
+ True if terminal supports UTF-8, False otherwise
36
+ """
37
+ # Hard stop: classic Windows console (conhost)
38
+ if os.name == "nt" and "WT_SESSION" not in os.environ:
39
+ return False
40
+
41
+ # Encoding check
42
+ encoding = sys.stdout.encoding
43
+ if not encoding:
44
+ return False
45
+
46
+ try:
47
+ "✓✗".encode(encoding)
48
+ return True
49
+ except UnicodeEncodeError:
50
+ return False
51
+
52
+
53
+ def get_action_success_string() -> str:
54
+ """Get the appropriate success symbol based on terminal capabilities.
55
+
56
+ Returns:
57
+ Unicode tick symbol (✓) if terminal supports UTF-8, otherwise "[ OK ]"
58
+ """
59
+ return "✓" if _supports_unicode() else "[ OK ]"
60
+
61
+
62
+ def get_action_failure_string() -> str:
63
+ """Get the appropriate failure symbol based on terminal capabilities.
64
+
65
+ Returns:
66
+ Unicode cross symbol (✗) if terminal supports UTF-8, otherwise "[ FAIL ]"
67
+ """
68
+ return "✗" if _supports_unicode() else "[ FAIL ]"
69
+
70
+
29
71
  def _format_task_arguments(arg_specs: list[str | dict]) -> str:
30
72
  """Format task arguments for display in list output.
31
73
 
@@ -324,7 +366,7 @@ def _clean_state(tasks_file: Optional[str] = None) -> None:
324
366
 
325
367
  if state_path.exists():
326
368
  state_path.unlink()
327
- console.print(f"[green] Removed {state_path}[/green]")
369
+ console.print(f"[green]{get_action_success_string()} Removed {state_path}[/green]")
328
370
  console.print("All tasks will run fresh on next execution")
329
371
  else:
330
372
  console.print(f"[yellow]No state file found at {state_path}[/yellow]")
@@ -410,9 +452,9 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
410
452
  state.save()
411
453
  try:
412
454
  executor.execute_task(task_name, args_dict, force=force, only=only)
413
- console.print(f"[green] Task '{task_name}' completed successfully[/green]")
455
+ console.print(f"[green]{get_action_success_string()} Task '{task_name}' completed successfully[/green]")
414
456
  except Exception as e:
415
- console.print(f"[red] Task '{task_name}' failed: {e}[/red]")
457
+ console.print(f"[red]{get_action_failure_string()} Task '{task_name}' failed: {e}[/red]")
416
458
  raise typer.Exit(1)
417
459
 
418
460
 
tasktree/substitution.py CHANGED
@@ -5,6 +5,7 @@ and {{ env.NAME }} placeholders with their corresponding values.
5
5
  """
6
6
 
7
7
  import re
8
+ from random import choice
8
9
  from typing import Any
9
10
 
10
11
 
@@ -15,11 +16,11 @@ PLACEHOLDER_PATTERN = re.compile(
15
16
  )
16
17
 
17
18
 
18
- def substitute_variables(text: str, variables: dict[str, str]) -> str:
19
+ def substitute_variables(text: str | dict[str, Any], variables: dict[str, str]) -> str | dict[str, Any]:
19
20
  """Substitute {{ var.name }} placeholders with variable values.
20
21
 
21
22
  Args:
22
- text: Text containing {{ var.name }} placeholders
23
+ text: Text containing {{ var.name }} placeholders, or an argument dict with elements to be substituted
23
24
  variables: Dictionary mapping variable names to their string values
24
25
 
25
26
  Returns:
@@ -28,23 +29,42 @@ def substitute_variables(text: str, variables: dict[str, str]) -> str:
28
29
  Raises:
29
30
  ValueError: If a referenced variable is not defined
30
31
  """
31
- def replace_match(match: re.Match) -> str:
32
- prefix = match.group(1)
33
- name = match.group(2)
34
-
35
- # Only substitute var: placeholders
36
- if prefix != "var":
37
- return match.group(0) # Return unchanged
38
-
39
- if name not in variables:
40
- raise ValueError(
41
- f"Variable '{name}' is not defined. "
42
- f"Variables must be defined before use."
43
- )
44
-
45
- return variables[name]
46
-
47
- return PLACEHOLDER_PATTERN.sub(replace_match, text)
32
+ if isinstance(text, dict):
33
+ # The dict will only contain a single key, the value of this key should also be a dictionary, which contains
34
+ # the actual details of the argument.
35
+ assert len(text.keys()) == 1
36
+
37
+ for arg_name in text.keys():
38
+ # Pull out and substitute the individual fields of an argument one at a time
39
+ for field in [ "default", "min", "max" ]:
40
+ if field in text[arg_name]:
41
+ text[arg_name][field] = substitute_variables(text[arg_name][field], variables)
42
+
43
+ # choices is a list of things
44
+ if "choices" in text[arg_name]:
45
+ text[arg_name]["choices"] = [substitute_variables(c, variables) for c in text[arg_name]["choices"]]
46
+
47
+ return text
48
+ else:
49
+ raise ValueError("Empty arg dictionary")
50
+ else:
51
+ def replace_match(match: re.Match) -> str:
52
+ prefix = match.group(1)
53
+ name = match.group(2)
54
+
55
+ # Only substitute var: placeholders
56
+ if prefix != "var":
57
+ return match.group(0) # Return unchanged
58
+
59
+ if name not in variables:
60
+ raise ValueError(
61
+ f"Variable '{name}' is not defined. "
62
+ f"Variables must be defined before use."
63
+ )
64
+
65
+ return variables[name]
66
+
67
+ return PLACEHOLDER_PATTERN.sub(replace_match, text)
48
68
 
49
69
 
50
70
  def substitute_arguments(text: str, args: dict[str, Any], exported_args: set[str] | None = None) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.12
3
+ Version: 0.0.14
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -1,15 +1,15 @@
1
1
  tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
2
- tasktree/cli.py,sha256=uL4RGap1U7-_4mcdEGbsELR4cvm1aUaqbvnX8XJFNKc,17652
2
+ tasktree/cli.py,sha256=ywaLXEehOHcMsBioL4RxPQ1vTWIG1V7Hb0TZ1YI2F_g,18820
3
3
  tasktree/docker.py,sha256=qvja8G63uAcC73YMVY739egda1_CcBtoqzm0qIJU_Q8,14443
4
4
  tasktree/executor.py,sha256=Q7Bks5B88i-IyZDpxGSps9MM3uflz0U3yn4Rtq_uHMM,42266
5
5
  tasktree/graph.py,sha256=oXLxX0Ix4zSkVBg8_3x9K7WxSFpg136sp4MF-d2mDEQ,9682
6
6
  tasktree/hasher.py,sha256=0GrnCfwAXnwq_kpnHFFb12B5_2VFNXx6Ng7hTdcCyXo,4415
7
7
  tasktree/parser.py,sha256=XdFuELqrrhHc45HeMpo6-gflopZM7kYTqO1lQcFwtFk,68782
8
8
  tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
9
- tasktree/substitution.py,sha256=M_qcP0NKJATrKcNShSqHJatneuth1RVwTk1ci8-ZuxQ,6473
9
+ tasktree/substitution.py,sha256=UAq69YO4uGkvFfd5mtGy6CwFZ_uxZGf4HGpnvCBVBuc,7499
10
10
  tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
11
11
  tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
12
- tasktree-0.0.12.dist-info/METADATA,sha256=fdh62_US58sghqn46-f0072dEnFECrJccRa6tL7m2DU,37234
13
- tasktree-0.0.12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- tasktree-0.0.12.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
15
- tasktree-0.0.12.dist-info/RECORD,,
12
+ tasktree-0.0.14.dist-info/METADATA,sha256=JnlgocAOtAq_Q-vWrOkYM6mofrCeH8xiGFKYqaQmX4M,37234
13
+ tasktree-0.0.14.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ tasktree-0.0.14.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
15
+ tasktree-0.0.14.dist-info/RECORD,,