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.
@@ -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]