tasktree 0.0.7__py3-none-any.whl → 0.0.9__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 +78 -22
- tasktree/docker.py +25 -0
- tasktree/executor.py +346 -34
- tasktree/graph.py +124 -26
- tasktree/hasher.py +73 -2
- tasktree/parser.py +1288 -35
- tasktree/substitution.py +198 -0
- tasktree/types.py +11 -2
- tasktree-0.0.9.dist-info/METADATA +1240 -0
- tasktree-0.0.9.dist-info/RECORD +15 -0
- tasktree-0.0.7.dist-info/METADATA +0 -654
- tasktree-0.0.7.dist-info/RECORD +0 -14
- {tasktree-0.0.7.dist-info → tasktree-0.0.9.dist-info}/WHEEL +0 -0
- {tasktree-0.0.7.dist-info → tasktree-0.0.9.dist-info}/entry_points.txt +0 -0
tasktree/substitution.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Placeholder substitution for variables, arguments, and environment variables.
|
|
2
|
+
|
|
3
|
+
This module provides functions to substitute {{ var.name }}, {{ arg.name }},
|
|
4
|
+
and {{ env.NAME }} placeholders with their corresponding values.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Pattern matches: {{ prefix.name }} with optional whitespace
|
|
12
|
+
# Groups: (1) prefix (var|arg|env|tt), (2) name (identifier)
|
|
13
|
+
PLACEHOLDER_PATTERN = re.compile(
|
|
14
|
+
r'\{\{\s*(var|arg|env|tt)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def substitute_variables(text: str, variables: dict[str, str]) -> str:
|
|
19
|
+
"""Substitute {{ var.name }} placeholders with variable values.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
text: Text containing {{ var.name }} placeholders
|
|
23
|
+
variables: Dictionary mapping variable names to their string values
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Text with all {{ var.name }} placeholders replaced
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If a referenced variable is not defined
|
|
30
|
+
"""
|
|
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)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def substitute_arguments(text: str, args: dict[str, Any], exported_args: set[str] | None = None) -> str:
|
|
51
|
+
"""Substitute {{ arg.name }} placeholders with argument values.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
text: Text containing {{ arg.name }} placeholders
|
|
55
|
+
args: Dictionary mapping argument names to their values
|
|
56
|
+
exported_args: Set of argument names that are exported (not available for substitution)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Text with all {{ arg.name }} placeholders replaced
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If a referenced argument is not provided or is exported
|
|
63
|
+
"""
|
|
64
|
+
# Use empty set if None for cleaner handling
|
|
65
|
+
exported_args = exported_args or set()
|
|
66
|
+
|
|
67
|
+
def replace_match(match: re.Match) -> str:
|
|
68
|
+
prefix = match.group(1)
|
|
69
|
+
name = match.group(2)
|
|
70
|
+
|
|
71
|
+
# Only substitute arg: placeholders
|
|
72
|
+
if prefix != "arg":
|
|
73
|
+
return match.group(0) # Return unchanged
|
|
74
|
+
|
|
75
|
+
# Check if argument is exported (not available for substitution)
|
|
76
|
+
if name in exported_args:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Argument '{name}' is exported (defined as ${name}) and cannot be used in template substitution\n"
|
|
79
|
+
f"Template: {{{{ arg.{name} }}}}\n\n"
|
|
80
|
+
f"Exported arguments are available as environment variables:\n"
|
|
81
|
+
f" cmd: ... ${name} ..."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if name not in args:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Argument '{name}' is not defined. "
|
|
87
|
+
f"Required arguments must be provided."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Convert to string (lowercase for booleans to match YAML/shell conventions)
|
|
91
|
+
value = args[name]
|
|
92
|
+
if isinstance(value, bool):
|
|
93
|
+
return str(value).lower()
|
|
94
|
+
return str(value)
|
|
95
|
+
|
|
96
|
+
return PLACEHOLDER_PATTERN.sub(replace_match, text)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def substitute_environment(text: str) -> str:
|
|
100
|
+
"""Substitute {{ env.NAME }} placeholders with environment variable values.
|
|
101
|
+
|
|
102
|
+
Environment variables are read from os.environ at substitution time.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
text: Text containing {{ env.NAME }} placeholders
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Text with all {{ env.NAME }} placeholders replaced
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If a referenced environment variable is not set
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> os.environ['USER'] = 'alice'
|
|
115
|
+
>>> substitute_environment("Hello {{ env.USER }}")
|
|
116
|
+
'Hello alice'
|
|
117
|
+
"""
|
|
118
|
+
import os
|
|
119
|
+
|
|
120
|
+
def replace_match(match: re.Match) -> str:
|
|
121
|
+
prefix = match.group(1)
|
|
122
|
+
name = match.group(2)
|
|
123
|
+
|
|
124
|
+
# Only substitute env: placeholders
|
|
125
|
+
if prefix != "env":
|
|
126
|
+
return match.group(0) # Return unchanged
|
|
127
|
+
|
|
128
|
+
value = os.environ.get(name)
|
|
129
|
+
if value is None:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
f"Environment variable '{name}' is not set"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return value
|
|
135
|
+
|
|
136
|
+
return PLACEHOLDER_PATTERN.sub(replace_match, text)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def substitute_builtin_variables(text: str, builtin_vars: dict[str, str]) -> str:
|
|
140
|
+
"""Substitute {{ tt.name }} placeholders with built-in variable values.
|
|
141
|
+
|
|
142
|
+
Built-in variables are system-provided values that tasks can reference.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
text: Text containing {{ tt.name }} placeholders
|
|
146
|
+
builtin_vars: Dictionary mapping built-in variable names to their string values
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Text with all {{ tt.name }} placeholders replaced
|
|
150
|
+
|
|
151
|
+
Raises:
|
|
152
|
+
ValueError: If a referenced built-in variable is not defined
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
>>> builtin_vars = {'project_root': '/home/user/project', 'task_name': 'build'}
|
|
156
|
+
>>> substitute_builtin_variables("Root: {{ tt.project_root }}", builtin_vars)
|
|
157
|
+
'Root: /home/user/project'
|
|
158
|
+
"""
|
|
159
|
+
def replace_match(match: re.Match) -> str:
|
|
160
|
+
prefix = match.group(1)
|
|
161
|
+
name = match.group(2)
|
|
162
|
+
|
|
163
|
+
# Only substitute tt: placeholders
|
|
164
|
+
if prefix != "tt":
|
|
165
|
+
return match.group(0) # Return unchanged
|
|
166
|
+
|
|
167
|
+
if name not in builtin_vars:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
f"Built-in variable '{{ tt.{name} }}' is not defined. "
|
|
170
|
+
f"Available built-in variables: {', '.join(sorted(builtin_vars.keys()))}"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return builtin_vars[name]
|
|
174
|
+
|
|
175
|
+
return PLACEHOLDER_PATTERN.sub(replace_match, text)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def substitute_all(text: str, variables: dict[str, str], args: dict[str, Any]) -> str:
|
|
179
|
+
"""Substitute all placeholder types: variables, arguments, environment.
|
|
180
|
+
|
|
181
|
+
Substitution order: variables → arguments → environment.
|
|
182
|
+
This allows variables to contain arg/env placeholders.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
text: Text containing placeholders
|
|
186
|
+
variables: Dictionary mapping variable names to their string values
|
|
187
|
+
args: Dictionary mapping argument names to their values
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Text with all placeholders replaced
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ValueError: If any referenced variable, argument, or environment variable is not defined
|
|
194
|
+
"""
|
|
195
|
+
text = substitute_variables(text, variables)
|
|
196
|
+
text = substitute_arguments(text, args)
|
|
197
|
+
text = substitute_environment(text)
|
|
198
|
+
return text
|
tasktree/types.py
CHANGED
|
@@ -113,11 +113,13 @@ TYPE_MAPPING = {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
def get_click_type(type_name: str) -> click.ParamType:
|
|
117
|
-
"""Get Click parameter type by name.
|
|
116
|
+
def get_click_type(type_name: str, min_val: int | float | None = None, max_val: int | float | None = None) -> click.ParamType:
|
|
117
|
+
"""Get Click parameter type by name with optional range constraints.
|
|
118
118
|
|
|
119
119
|
Args:
|
|
120
120
|
type_name: Type name from task definition (e.g., 'str', 'int', 'hostname')
|
|
121
|
+
min_val: Optional minimum value for numeric types
|
|
122
|
+
max_val: Optional maximum value for numeric types
|
|
121
123
|
|
|
122
124
|
Returns:
|
|
123
125
|
Click parameter type instance
|
|
@@ -127,4 +129,11 @@ def get_click_type(type_name: str) -> click.ParamType:
|
|
|
127
129
|
"""
|
|
128
130
|
if type_name not in TYPE_MAPPING:
|
|
129
131
|
raise ValueError(f"Unknown type: {type_name}")
|
|
132
|
+
|
|
133
|
+
# For int and float types, apply range constraints if specified
|
|
134
|
+
if type_name == "int" and (min_val is not None or max_val is not None):
|
|
135
|
+
return click.IntRange(min=min_val, max=max_val)
|
|
136
|
+
elif type_name == "float" and (min_val is not None or max_val is not None):
|
|
137
|
+
return click.FloatRange(min=min_val, max=max_val)
|
|
138
|
+
|
|
130
139
|
return TYPE_MAPPING[type_name]
|