tasktree 0.0.21__py3-none-any.whl → 0.0.23__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/graph.py CHANGED
@@ -4,7 +4,6 @@ Dependency resolution using topological sorting.
4
4
  """
5
5
 
6
6
  from graphlib import TopologicalSorter
7
- from pathlib import Path
8
7
  from typing import Any
9
8
 
10
9
  from tasktree.hasher import hash_args
@@ -13,7 +12,6 @@ from tasktree.parser import (
13
12
  Task,
14
13
  DependencyInvocation,
15
14
  parse_dependency_spec,
16
- parse_arg_spec,
17
15
  )
18
16
  from tasktree.substitution import substitute_dependency_args
19
17
 
@@ -58,7 +56,7 @@ def resolve_dependency_invocation(
58
56
  parent_task_name: str,
59
57
  parent_args: dict[str, Any],
60
58
  parent_exported_args: set[str],
61
- recipe: Recipe
59
+ recipe: Recipe,
62
60
  ) -> DependencyInvocation:
63
61
  """
64
62
  Parse dependency specification and substitute parent argument templates.
@@ -100,7 +98,7 @@ def resolve_dependency_invocation(
100
98
  ... recipe
101
99
  ... )
102
100
  DependencyInvocation("build", {"mode": "production"})
103
- @athena: 968bae796809
101
+ @athena: dd9a84821159
104
102
  """
105
103
  # Simple string case - no args to substitute
106
104
  if isinstance(dep_spec, str):
@@ -246,9 +244,7 @@ class TaskNode:
246
244
 
247
245
 
248
246
  def resolve_execution_order(
249
- recipe: Recipe,
250
- target_task: str,
251
- target_args: dict[str, Any] | None = None
247
+ recipe: Recipe, target_task: str, target_args: dict[str, Any] | None = None
252
248
  ) -> list[tuple[str, dict[str, Any]]]:
253
249
  """
254
250
  Resolve execution order for a task and its dependencies.
@@ -264,7 +260,7 @@ def resolve_execution_order(
264
260
  Raises:
265
261
  TaskNotFoundError: If target task or any dependency doesn't exist
266
262
  CycleError: If a dependency cycle is detected
267
- @athena: b4443e1cb45d
263
+ @athena: 687f627efc75
268
264
  """
269
265
  if target_task not in recipe.tasks:
270
266
  raise TaskNotFoundError(f"Task not found: {target_task}")
@@ -273,7 +269,9 @@ def resolve_execution_order(
273
269
  graph: dict[TaskNode, set[TaskNode]] = {}
274
270
 
275
271
  # Track seen nodes to detect duplicates
276
- seen_invocations: dict[tuple[str, str], TaskNode] = {} # (task_name, args_hash) -> node
272
+ seen_invocations: dict[
273
+ tuple[str, str], TaskNode
274
+ ] = {} # (task_name, args_hash) -> node
277
275
 
278
276
  def get_or_create_node(task_name: str, args: dict[str, Any] | None) -> TaskNode:
279
277
  """Get existing node or create new one for this invocation."""
@@ -307,7 +305,7 @@ def resolve_execution_order(
307
305
  parent_task_name=node.task_name,
308
306
  parent_args=node.args or {},
309
307
  parent_exported_args=parent_exported_args,
310
- recipe=recipe
308
+ recipe=recipe,
311
309
  )
312
310
 
313
311
  # Create or get node for this dependency invocation
@@ -592,7 +590,9 @@ def get_implicit_inputs(recipe: Recipe, task: Task) -> list[str]:
592
590
  return implicit_inputs
593
591
 
594
592
 
595
- def build_dependency_tree(recipe: Recipe, target_task: str, target_args: dict[str, Any] | None = None) -> dict:
593
+ def build_dependency_tree(
594
+ recipe: Recipe, target_task: str, target_args: dict[str, Any] | None = None
595
+ ) -> dict:
596
596
  """
597
597
  Build a tree structure representing dependencies for visualization.
598
598
 
@@ -607,7 +607,7 @@ def build_dependency_tree(recipe: Recipe, target_task: str, target_args: dict[st
607
607
 
608
608
  Returns:
609
609
  Nested dictionary representing the dependency tree
610
- @athena: 570e5c663887
610
+ @athena: 8c853b393bdb
611
611
  """
612
612
  if target_task not in recipe.tasks:
613
613
  raise TaskNotFoundError(f"Task not found: {target_task}")
@@ -622,12 +622,17 @@ def build_dependency_tree(recipe: Recipe, target_task: str, target_args: dict[st
622
622
 
623
623
  # Create node identifier for cycle detection
624
624
  from tasktree.hasher import hash_args
625
+
625
626
  args_dict = args or {}
626
627
  node_id = (task_name, hash_args(args_dict) if args_dict else "")
627
628
 
628
629
  # Detect cycles in current recursion path
629
630
  if node_id in current_path:
630
- display_name = task_name if not args_dict else f"{task_name}({', '.join(f'{k}={v}' for k, v in sorted(args_dict.items()))})"
631
+ display_name = (
632
+ task_name
633
+ if not args_dict
634
+ else f"{task_name}({', '.join(f'{k}={v}' for k, v in sorted(args_dict.items()))})"
635
+ )
631
636
  return {"name": display_name, "deps": [], "cycle": True}
632
637
 
633
638
  current_path.add(node_id)
tasktree/hasher.py CHANGED
@@ -28,17 +28,19 @@ def _arg_sort_key(arg: str | dict[str, Any]) -> str:
28
28
  return arg
29
29
 
30
30
 
31
- def _normalize_choices_lists(args: list[str | dict[str, Any]]) -> list[str | dict[str, Any]]:
31
+ def _normalize_choices_lists(
32
+ args: list[str | dict[str, Any]],
33
+ ) -> list[str | dict[str, Any]]:
32
34
  """
33
35
  Normalize argument choices lists by sorting them for deterministic hashing.
34
36
 
35
37
  Args:
36
- args: List of argument specifications (strings or dicts)
38
+ args: List of argument specifications (strings or dicts)
37
39
 
38
40
  Returns:
39
- List of argument specs with sorted choices lists
41
+ List of argument specs with sorted choices lists
40
42
 
41
- @athena: 7512379275e3
43
+ @athena: dc34b6fb09a6
42
44
  """
43
45
  normalized_args = []
44
46
  for arg in args:
@@ -46,8 +48,11 @@ def _normalize_choices_lists(args: list[str | dict[str, Any]]) -> list[str | di
46
48
  # Deep copy and sort choices if present
47
49
  normalized = {}
48
50
  for key, value in arg.items():
49
- if isinstance(value, dict) and 'choices' in value:
50
- normalized[key] = {**value, 'choices': sorted(value['choices'], key=str)}
51
+ if isinstance(value, dict) and "choices" in value:
52
+ normalized[key] = {
53
+ **value,
54
+ "choices": sorted(value["choices"], key=str),
55
+ }
51
56
  else:
52
57
  normalized[key] = value
53
58
  normalized_args.append(normalized)
@@ -94,7 +99,7 @@ def hash_task(
94
99
  working_dir: str,
95
100
  args: list[str | dict[str, Any]],
96
101
  env: str = "",
97
- deps: list[str | dict[str, Any]] | None = None
102
+ deps: list[str | dict[str, Any]] | None = None,
98
103
  ) -> str:
99
104
  """
100
105
  Hash task definition including dependencies.
@@ -109,7 +114,7 @@ def hash_task(
109
114
 
110
115
  Returns:
111
116
  8-character hash of task definition
112
- @athena: 7a461d51a8bb
117
+ @athena: 815a47cf1af0
113
118
  """
114
119
  data = {
115
120
  "cmd": cmd,
@@ -134,7 +139,10 @@ def hash_task(
134
139
  else:
135
140
  normalized_deps.append(dep)
136
141
  # Sort using JSON serialization for consistent ordering
137
- data["deps"] = sorted(normalized_deps, key=lambda x: json.dumps(x, sort_keys=True) if isinstance(x, dict) else x)
142
+ data["deps"] = sorted(
143
+ normalized_deps,
144
+ key=lambda x: json.dumps(x, sort_keys=True) if isinstance(x, dict) else x,
145
+ )
138
146
 
139
147
  serialized = json.dumps(data, sort_keys=True, separators=(",", ":"))
140
148
  return hashlib.sha256(serialized.encode()).hexdigest()[:8]
@@ -165,10 +173,9 @@ def hash_environment_definition(env) -> str:
165
173
 
166
174
  Returns:
167
175
  16-character hash of environment definition
168
- @athena: 2de34f1a0b4a
176
+ @athena: a54f6c171ba9
169
177
  """
170
178
  # Import inside function to avoid circular dependency
171
- from tasktree.parser import Environment
172
179
 
173
180
  # Handle args - can be list (shell args) or dict (docker build args)
174
181
  args_value = env.args
tasktree/logging.py ADDED
@@ -0,0 +1,112 @@
1
+ """Logging infrastructure for Task Tree.
2
+
3
+ Provides a Logger interface for dependency injection of logging functionality.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import enum
9
+ from abc import abstractmethod
10
+
11
+
12
+ class LogLevel(enum.Enum):
13
+ """
14
+ Log verbosity levels for tasktree diagnostic messages.
15
+
16
+ Lower numeric values represent higher severity / less verbosity.
17
+ @athena: 3b2d8a378440
18
+ """
19
+
20
+ FATAL = 0 # Only unrecoverable errors (malformed task files, missing dependencies)
21
+ ERROR = 1 # Fatal errors plus task execution failures
22
+ WARN = 2 # Errors plus warnings about deprecated features, configuration issues
23
+ INFO = 3 # Warnings plus normal execution progress (default)
24
+ DEBUG = 4 # Info plus variable values, resolved paths, environment details
25
+ TRACE = 5 # Debug plus fine-grained execution tracing
26
+
27
+
28
+ class Logger:
29
+ """
30
+ Abstract base class for logging implementations.
31
+
32
+ Provides a level-based logging interface with stack-based level management.
33
+ Concrete implementations must define how messages are output (e.g., to console, file, etc.).
34
+ @athena: fdcd08796011
35
+ """
36
+
37
+ @abstractmethod
38
+ def log(self, level: LogLevel, *args, **kwargs) -> None:
39
+ """
40
+ Log a message at the specified level.
41
+
42
+ Args:
43
+ level: The severity level of the message
44
+ *args: Positional arguments to log (strings, Rich objects, etc.)
45
+ **kwargs: Keyword arguments for formatting (e.g., style, justify)
46
+ @athena: 4563ae920ff4
47
+ """
48
+ pass
49
+
50
+ @abstractmethod
51
+ def push_level(self, level: LogLevel) -> None:
52
+ """
53
+ Push a new log level onto the level stack.
54
+
55
+ Messages below this level will be filtered out until pop_level() is called.
56
+ Useful for temporarily increasing verbosity in specific code sections.
57
+
58
+ Args:
59
+ level: The new log level to use
60
+ @athena: c73ac375d30a
61
+ """
62
+ pass
63
+
64
+ @abstractmethod
65
+ def pop_level(self) -> LogLevel:
66
+ """
67
+ Pop the current log level from the stack and return to the previous level.
68
+
69
+ Returns:
70
+ The log level that was popped
71
+
72
+ Raises:
73
+ RuntimeError: If attempting to pop the base (initial) log level
74
+ @athena: d4721a34516f
75
+ """
76
+ pass
77
+
78
+ def fatal(self, *args, **kwargs):
79
+ """
80
+ @athena: 951c9c5d4f3b
81
+ """
82
+ self.log(LogLevel.FATAL, *args, **kwargs)
83
+
84
+ def error(self, *args, **kwargs):
85
+ """
86
+ @athena: 7125264a8ce1
87
+ """
88
+ self.log(LogLevel.ERROR, *args, **kwargs)
89
+
90
+ def warn(self, *args, **kwargs):
91
+ """
92
+ @athena: 5a70af1bb5b4
93
+ """
94
+ self.log(LogLevel.WARN, *args, **kwargs)
95
+
96
+ def info(self, *args, **kwargs):
97
+ """
98
+ @athena: cdb5381023a6
99
+ """
100
+ self.log(LogLevel.INFO, *args, **kwargs)
101
+
102
+ def debug(self, *args, **kwargs):
103
+ """
104
+ @athena: ddfd0e359f09
105
+ """
106
+ self.log(LogLevel.DEBUG, *args, **kwargs)
107
+
108
+ def trace(self, *args, **kwargs):
109
+ """
110
+ @athena: 4f615a15562c
111
+ """
112
+ self.log(LogLevel.TRACE, *args, **kwargs)