tasktree 0.0.19__py3-none-any.whl → 0.0.21__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/state.py CHANGED
@@ -10,13 +10,19 @@ from typing import Any
10
10
 
11
11
  @dataclass
12
12
  class TaskState:
13
- """State for a single task execution."""
13
+ """
14
+ State for a single task execution.
15
+ @athena: b08a937b7f2f
16
+ """
14
17
 
15
18
  last_run: float
16
19
  input_state: dict[str, float | str] = field(default_factory=dict)
17
20
 
18
21
  def to_dict(self) -> dict[str, Any]:
19
- """Convert to dictionary for JSON serialization."""
22
+ """
23
+ Convert to dictionary for JSON serialization.
24
+ @athena: 5f42efc35e77
25
+ """
20
26
  return {
21
27
  "last_run": self.last_run,
22
28
  "input_state": self.input_state,
@@ -24,7 +30,10 @@ class TaskState:
24
30
 
25
31
  @classmethod
26
32
  def from_dict(cls, data: dict[str, Any]) -> "TaskState":
27
- """Create from dictionary loaded from JSON."""
33
+ """
34
+ Create from dictionary loaded from JSON.
35
+ @athena: d9237db7e7e7
36
+ """
28
37
  return cls(
29
38
  last_run=data["last_run"],
30
39
  input_state=data.get("input_state", {}),
@@ -32,15 +41,20 @@ class TaskState:
32
41
 
33
42
 
34
43
  class StateManager:
35
- """Manages the .tasktree-state file."""
44
+ """
45
+ Manages the .tasktree-state file.
46
+ @athena: 44713c70e04e
47
+ """
36
48
 
37
49
  STATE_FILE = ".tasktree-state"
38
50
 
39
51
  def __init__(self, project_root: Path):
40
- """Initialize state manager.
52
+ """
53
+ Initialize state manager.
41
54
 
42
55
  Args:
43
- project_root: Root directory of the project
56
+ project_root: Root directory of the project
57
+ @athena: a0afbd8ae591
44
58
  """
45
59
  self.project_root = project_root
46
60
  self.state_path = project_root / self.STATE_FILE
@@ -48,7 +62,10 @@ class StateManager:
48
62
  self._loaded = False
49
63
 
50
64
  def load(self) -> None:
51
- """Load state from file if it exists."""
65
+ """
66
+ Load state from file if it exists.
67
+ @athena: 11748af0886c
68
+ """
52
69
  if self.state_path.exists():
53
70
  try:
54
71
  with open(self.state_path, "r") as f:
@@ -63,40 +80,49 @@ class StateManager:
63
80
  self._loaded = True
64
81
 
65
82
  def save(self) -> None:
66
- """Save state to file."""
83
+ """
84
+ Save state to file.
85
+ @athena: 11e4a9761e4d
86
+ """
67
87
  data = {key: value.to_dict() for key, value in self._state.items()}
68
88
  with open(self.state_path, "w") as f:
69
89
  json.dump(data, f, indent=2)
70
90
 
71
91
  def get(self, cache_key: str) -> TaskState | None:
72
- """Get state for a task.
92
+ """
93
+ Get state for a task.
73
94
 
74
95
  Args:
75
- cache_key: Cache key (task_hash or task_hash__args_hash)
96
+ cache_key: Cache key (task_hash or task_hash__args_hash)
76
97
 
77
98
  Returns:
78
- TaskState if found, None otherwise
99
+ TaskState if found, None otherwise
100
+ @athena: fe5b27e855eb
79
101
  """
80
102
  if not self._loaded:
81
103
  self.load()
82
104
  return self._state.get(cache_key)
83
105
 
84
106
  def set(self, cache_key: str, state: TaskState) -> None:
85
- """Set state for a task.
107
+ """
108
+ Set state for a task.
86
109
 
87
110
  Args:
88
- cache_key: Cache key (task_hash or task_hash__args_hash)
89
- state: TaskState to store
111
+ cache_key: Cache key (task_hash or task_hash__args_hash)
112
+ state: TaskState to store
113
+ @athena: 244f16ea0ebc
90
114
  """
91
115
  if not self._loaded:
92
116
  self.load()
93
117
  self._state[cache_key] = state
94
118
 
95
119
  def prune(self, valid_task_hashes: set[str]) -> None:
96
- """Remove state entries for tasks that no longer exist.
120
+ """
121
+ Remove state entries for tasks that no longer exist.
97
122
 
98
123
  Args:
99
- valid_task_hashes: Set of valid task hashes from current recipe
124
+ valid_task_hashes: Set of valid task hashes from current recipe
125
+ @athena: ce21bb523d49
100
126
  """
101
127
  if not self._loaded:
102
128
  self.load()
@@ -114,6 +140,9 @@ class StateManager:
114
140
  del self._state[key]
115
141
 
116
142
  def clear(self) -> None:
117
- """Clear all state (useful for testing)."""
143
+ """
144
+ Clear all state (useful for testing).
145
+ @athena: 3a92e36d9f83
146
+ """
118
147
  self._state = {}
119
148
  self._loaded = True
tasktree/substitution.py CHANGED
@@ -1,7 +1,9 @@
1
- """Placeholder substitution for variables, arguments, and environment variables.
1
+ """
2
+ Placeholder substitution for variables, arguments, and environment variables.
2
3
 
3
4
  This module provides functions to substitute {{ var.name }}, {{ arg.name }},
4
5
  and {{ env.NAME }} placeholders with their corresponding values.
6
+ @athena: f92441b6fff5
5
7
  """
6
8
 
7
9
  import re
@@ -12,28 +14,36 @@ from typing import Any
12
14
  # Pattern matches: {{ prefix.name }} with optional whitespace
13
15
  # Groups: (1) prefix (var|arg|env|tt), (2) name (identifier)
14
16
  PLACEHOLDER_PATTERN = re.compile(
15
- r'\{\{\s*(var|arg|env|tt)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
17
+ r'\{\{\s*(var|arg|env|tt)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
16
18
  )
17
19
 
18
20
  # Pattern matches: {{ dep.task_name.outputs.output_name }} with optional whitespace
19
21
  # Groups: (1) task_name (can include dots for namespacing), (2) output_name (identifier)
20
22
  DEP_OUTPUT_PATTERN = re.compile(
21
- r'\{\{\s*dep\s*\.\s*([a-zA-Z_][a-zA-Z0-9_.-]*)\s*\.\s*outputs\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
23
+ r'\{\{\s*dep\.([a-zA-Z_][a-zA-Z0-9_.-]*)\.outputs\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
24
+ )
25
+
26
+ # Pattern matches: {{ self.(inputs|outputs).name }} or {{ self.(inputs|outputs).0 }} with optional whitespace
27
+ # Groups: (1) field (inputs|outputs), (2) name (identifier) or index (numeric)
28
+ SELF_REFERENCE_PATTERN = re.compile(
29
+ r'\{\{\s*self\.(inputs|outputs)\.([a-zA-Z_][a-zA-Z0-9_]*|[0-9]+)\s*\}\}'
22
30
  )
23
31
 
24
32
 
25
33
  def substitute_variables(text: str | dict[str, Any], variables: dict[str, str]) -> str | dict[str, Any]:
26
- """Substitute {{ var.name }} placeholders with variable values.
34
+ """
35
+ Substitute {{ var.name }} placeholders with variable values.
27
36
 
28
37
  Args:
29
- text: Text containing {{ var.name }} placeholders, or an argument dict with elements to be substituted
30
- variables: Dictionary mapping variable names to their string values
38
+ text: Text containing {{ var.name }} placeholders, or an argument dict with elements to be substituted
39
+ variables: Dictionary mapping variable names to their string values
31
40
 
32
41
  Returns:
33
- Text with all {{ var.name }} placeholders replaced
42
+ Text with all {{ var.name }} placeholders replaced
34
43
 
35
44
  Raises:
36
- ValueError: If a referenced variable is not defined
45
+ ValueError: If a referenced variable is not defined
46
+ @athena: 93e0fbbe447c
37
47
  """
38
48
  if isinstance(text, dict):
39
49
  # The dict will only contain a single key, the value of this key should also be a dictionary, which contains
@@ -79,18 +89,20 @@ def substitute_variables(text: str | dict[str, Any], variables: dict[str, str])
79
89
 
80
90
 
81
91
  def substitute_arguments(text: str, args: dict[str, Any], exported_args: set[str] | None = None) -> str:
82
- """Substitute {{ arg.name }} placeholders with argument values.
92
+ """
93
+ Substitute {{ arg.name }} placeholders with argument values.
83
94
 
84
95
  Args:
85
- text: Text containing {{ arg.name }} placeholders
86
- args: Dictionary mapping argument names to their values
87
- exported_args: Set of argument names that are exported (not available for substitution)
96
+ text: Text containing {{ arg.name }} placeholders
97
+ args: Dictionary mapping argument names to their values
98
+ exported_args: Set of argument names that are exported (not available for substitution)
88
99
 
89
100
  Returns:
90
- Text with all {{ arg.name }} placeholders replaced
101
+ Text with all {{ arg.name }} placeholders replaced
91
102
 
92
103
  Raises:
93
- ValueError: If a referenced argument is not provided or is exported
104
+ ValueError: If a referenced argument is not provided or is exported
105
+ @athena: 39577c2b74a6
94
106
  """
95
107
  # Use empty set if None for cleaner handling
96
108
  exported_args = exported_args or set()
@@ -128,23 +140,25 @@ def substitute_arguments(text: str, args: dict[str, Any], exported_args: set[str
128
140
 
129
141
 
130
142
  def substitute_environment(text: str) -> str:
131
- """Substitute {{ env.NAME }} placeholders with environment variable values.
143
+ """
144
+ Substitute {{ env.NAME }} placeholders with environment variable values.
132
145
 
133
146
  Environment variables are read from os.environ at substitution time.
134
147
 
135
148
  Args:
136
- text: Text containing {{ env.NAME }} placeholders
149
+ text: Text containing {{ env.NAME }} placeholders
137
150
 
138
151
  Returns:
139
- Text with all {{ env.NAME }} placeholders replaced
152
+ Text with all {{ env.NAME }} placeholders replaced
140
153
 
141
154
  Raises:
142
- ValueError: If a referenced environment variable is not set
155
+ ValueError: If a referenced environment variable is not set
143
156
 
144
157
  Example:
145
- >>> os.environ['USER'] = 'alice'
146
- >>> substitute_environment("Hello {{ env.USER }}")
147
- 'Hello alice'
158
+ >>> os.environ['USER'] = 'alice'
159
+ >>> substitute_environment("Hello {{ env.USER }}")
160
+ 'Hello alice'
161
+ @athena: 4f2afd2e0da2
148
162
  """
149
163
  import os
150
164
 
@@ -168,24 +182,26 @@ def substitute_environment(text: str) -> str:
168
182
 
169
183
 
170
184
  def substitute_builtin_variables(text: str, builtin_vars: dict[str, str]) -> str:
171
- """Substitute {{ tt.name }} placeholders with built-in variable values.
185
+ """
186
+ Substitute {{ tt.name }} placeholders with built-in variable values.
172
187
 
173
188
  Built-in variables are system-provided values that tasks can reference.
174
189
 
175
190
  Args:
176
- text: Text containing {{ tt.name }} placeholders
177
- builtin_vars: Dictionary mapping built-in variable names to their string values
191
+ text: Text containing {{ tt.name }} placeholders
192
+ builtin_vars: Dictionary mapping built-in variable names to their string values
178
193
 
179
194
  Returns:
180
- Text with all {{ tt.name }} placeholders replaced
195
+ Text with all {{ tt.name }} placeholders replaced
181
196
 
182
197
  Raises:
183
- ValueError: If a referenced built-in variable is not defined
198
+ ValueError: If a referenced built-in variable is not defined
184
199
 
185
200
  Example:
186
- >>> builtin_vars = {'project_root': '/home/user/project', 'task_name': 'build'}
187
- >>> substitute_builtin_variables("Root: {{ tt.project_root }}", builtin_vars)
188
- 'Root: /home/user/project'
201
+ >>> builtin_vars = {'project_root': '/home/user/project', 'task_name': 'build'}
202
+ >>> substitute_builtin_variables("Root: {{ tt.project_root }}", builtin_vars)
203
+ 'Root: /home/user/project'
204
+ @athena: 716250e3a71f
189
205
  """
190
206
  def replace_match(match: re.Match) -> str:
191
207
  prefix = match.group(1)
@@ -212,27 +228,29 @@ def substitute_dependency_args(
212
228
  parent_args: dict[str, Any],
213
229
  exported_args: set[str] | None = None
214
230
  ) -> str:
215
- """Substitute {{ arg.* }} templates in dependency argument values.
231
+ """
232
+ Substitute {{ arg.* }} templates in dependency argument values.
216
233
 
217
234
  This function substitutes parent task's arguments into dependency argument
218
235
  templates. Only {{ arg.* }} placeholders are allowed in dependency arguments.
219
236
 
220
237
  Args:
221
- template_value: String that may contain {{ arg.* }} placeholders
222
- parent_task_name: Name of parent task (for error messages)
223
- parent_args: Parent task's argument values
224
- exported_args: Set of parent's exported argument names
238
+ template_value: String that may contain {{ arg.* }} placeholders
239
+ parent_task_name: Name of parent task (for error messages)
240
+ parent_args: Parent task's argument values
241
+ exported_args: Set of parent's exported argument names
225
242
 
226
243
  Returns:
227
- String with {{ arg.* }} placeholders substituted
244
+ String with {{ arg.* }} placeholders substituted
228
245
 
229
246
  Raises:
230
- ValueError: If template references undefined arg, uses exported arg,
231
- or contains non-arg placeholders ({{ var.* }}, {{ env.* }}, {{ tt.* }})
247
+ ValueError: If template references undefined arg, uses exported arg,
248
+ or contains non-arg placeholders ({{ var.* }}, {{ env.* }}, {{ tt.* }})
232
249
 
233
250
  Example:
234
- >>> substitute_dependency_args("{{ arg.mode }}", "build", {"mode": "debug"})
235
- 'debug'
251
+ >>> substitute_dependency_args("{{ arg.mode }}", "build", {"mode": "debug"})
252
+ 'debug'
253
+ @athena: 3d07a1b4e6bc
236
254
  """
237
255
  # Check for disallowed placeholder types in dependency args
238
256
  # Only {{ arg.* }} is allowed, not {{ var.* }}, {{ env.* }}, or {{ tt.* }}
@@ -276,21 +294,23 @@ def substitute_dependency_args(
276
294
 
277
295
 
278
296
  def substitute_all(text: str, variables: dict[str, str], args: dict[str, Any]) -> str:
279
- """Substitute all placeholder types: variables, arguments, environment.
297
+ """
298
+ Substitute all placeholder types: variables, arguments, environment.
280
299
 
281
300
  Substitution order: variables → arguments → environment.
282
301
  This allows variables to contain arg/env placeholders.
283
302
 
284
303
  Args:
285
- text: Text containing placeholders
286
- variables: Dictionary mapping variable names to their string values
287
- args: Dictionary mapping argument names to their values
304
+ text: Text containing placeholders
305
+ variables: Dictionary mapping variable names to their string values
306
+ args: Dictionary mapping argument names to their values
288
307
 
289
308
  Returns:
290
- Text with all placeholders replaced
309
+ Text with all placeholders replaced
291
310
 
292
311
  Raises:
293
- ValueError: If any referenced variable, argument, or environment variable is not defined
312
+ ValueError: If any referenced variable, argument, or environment variable is not defined
313
+ @athena: c3fe48d3df5a
294
314
  """
295
315
  text = substitute_variables(text, variables)
296
316
  text = substitute_arguments(text, args)
@@ -304,7 +324,8 @@ def substitute_dependency_outputs(
304
324
  current_task_deps: list[str],
305
325
  resolved_tasks: dict[str, Any],
306
326
  ) -> str:
307
- """Substitute {{ dep.<task>.outputs.<name> }} placeholders with dependency output paths.
327
+ """
328
+ Substitute {{ dep.<task>.outputs.<name> }} placeholders with dependency output paths.
308
329
 
309
330
  This function resolves references to named outputs from dependency tasks.
310
331
  It validates that:
@@ -313,27 +334,28 @@ def substitute_dependency_outputs(
313
334
  - The referenced output name exists in the dependency task
314
335
 
315
336
  Args:
316
- text: Text containing {{ dep.*.outputs.* }} placeholders
317
- current_task_name: Name of task being resolved (for error messages)
318
- current_task_deps: List of dependency task names for the current task
319
- resolved_tasks: Dictionary mapping task names to Task objects (already resolved)
337
+ text: Text containing {{ dep.*.outputs.* }} placeholders
338
+ current_task_name: Name of task being resolved (for error messages)
339
+ current_task_deps: List of dependency task names for the current task
340
+ resolved_tasks: Dictionary mapping task names to Task objects (already resolved)
320
341
 
321
342
  Returns:
322
- Text with all {{ dep.*.outputs.* }} placeholders replaced with output paths
343
+ Text with all {{ dep.*.outputs.* }} placeholders replaced with output paths
323
344
 
324
345
  Raises:
325
- ValueError: If referenced task doesn't exist, isn't a dependency,
326
- or doesn't have the named output
346
+ ValueError: If referenced task doesn't exist, isn't a dependency,
347
+ or doesn't have the named output
327
348
 
328
349
  Example:
329
- >>> # Assuming build task has output { bundle: "dist/app.js" }
330
- >>> substitute_dependency_outputs(
331
- ... "Deploy {{ dep.build.outputs.bundle }}",
332
- ... "deploy",
333
- ... ["build"],
334
- ... {"build": build_task}
335
- ... )
336
- 'Deploy dist/app.js'
350
+ >>> # Assuming build task has output { bundle: "dist/app.js" }
351
+ >>> substitute_dependency_outputs(
352
+ ... "Deploy {{ dep.build.outputs.bundle }}",
353
+ ... "deploy",
354
+ ... ["build"],
355
+ ... {"build": build_task}
356
+ ... )
357
+ 'Deploy dist/app.js'
358
+ @athena: 1e537c8d579c
337
359
  """
338
360
  def replacer(match: re.Match) -> str:
339
361
  dep_task_name = match.group(1)
@@ -372,3 +394,113 @@ def substitute_dependency_outputs(
372
394
  return dep_task._output_map[output_name]
373
395
 
374
396
  return DEP_OUTPUT_PATTERN.sub(replacer, text)
397
+
398
+
399
+ def substitute_self_references(
400
+ text: str,
401
+ task_name: str,
402
+ input_map: dict[str, str],
403
+ output_map: dict[str, str],
404
+ indexed_inputs: list[str],
405
+ indexed_outputs: list[str],
406
+ ) -> str:
407
+ """
408
+ Substitute {{ self.inputs.name }} and {{ self.outputs.name }} placeholders.
409
+
410
+ This function resolves references to the task's own inputs and outputs.
411
+ Supports both named access ({{ self.inputs.name }}) and positional access
412
+ ({{ self.inputs.0 }}, {{ self.inputs.1 }}, etc.).
413
+
414
+ Named entries use the input_map/output_map dictionaries.
415
+ Positional entries use indexed_inputs/indexed_outputs lists (0-based indexing,
416
+ following YAML declaration order).
417
+
418
+ The substitution is literal string replacement - no glob expansion or path resolution.
419
+
420
+ Args:
421
+ text: Text containing {{ self.* }} placeholders
422
+ task_name: Name of current task (for error messages)
423
+ input_map: Dictionary mapping input names to path strings
424
+ output_map: Dictionary mapping output names to path strings
425
+ indexed_inputs: List of all inputs in YAML order
426
+ indexed_outputs: List of all outputs in YAML order
427
+
428
+ Returns:
429
+ Text with all {{ self.* }} placeholders replaced with literal path strings
430
+
431
+ Raises:
432
+ ValueError: If referenced name doesn't exist or index is out of bounds
433
+
434
+ Example:
435
+ >>> input_map = {"src": "*.txt"}
436
+ >>> output_map = {"dest": "out/result.txt"}
437
+ >>> indexed_inputs = ["*.txt"]
438
+ >>> indexed_outputs = ["out/result.txt"]
439
+ >>> substitute_self_references(
440
+ ... "cp {{ self.inputs.src }} {{ self.outputs.0 }}",
441
+ ... "copy",
442
+ ... input_map,
443
+ ... output_map,
444
+ ... indexed_inputs,
445
+ ... indexed_outputs
446
+ ... )
447
+ 'cp *.txt out/result.txt'
448
+ @athena: 9d997ff08eef
449
+ """
450
+ def replacer(match: re.Match) -> str:
451
+ field = match.group(1) # "inputs" or "outputs"
452
+ identifier = match.group(2) # name or numeric index
453
+
454
+ # Check if identifier is a numeric index
455
+ if identifier.isdigit():
456
+ # Positional access
457
+ index = int(identifier)
458
+ indexed_list = indexed_inputs if field == "inputs" else indexed_outputs
459
+ field_display = "input" if field == "inputs" else "output"
460
+
461
+ # Check if list is empty
462
+ if len(indexed_list) == 0:
463
+ raise ValueError(
464
+ f"Task '{task_name}' references {field_display} index '{index}' "
465
+ f"but has no {field} defined"
466
+ )
467
+
468
+ # Check bounds
469
+ if index >= len(indexed_list):
470
+ max_index = len(indexed_list) - 1
471
+ raise ValueError(
472
+ f"Task '{task_name}' references {field_display} index '{index}' "
473
+ f"but only has {len(indexed_list)} {field} (indices 0-{max_index})"
474
+ )
475
+
476
+ return indexed_list[index]
477
+ else:
478
+ # Named access (existing logic)
479
+ name = identifier
480
+
481
+ # Select appropriate map
482
+ if field == "inputs":
483
+ name_map = input_map
484
+ field_display = "input"
485
+ else: # field == "outputs"
486
+ name_map = output_map
487
+ field_display = "output"
488
+
489
+ # Check if name exists in map
490
+ if name not in name_map:
491
+ available = list(name_map.keys())
492
+ if available:
493
+ available_msg = ", ".join(available)
494
+ else:
495
+ available_msg = f"(none - all {field} are anonymous)"
496
+
497
+ raise ValueError(
498
+ f"Task '{task_name}' references {field_display} '{name}' "
499
+ f"but has no {field_display} named '{name}'.\n"
500
+ f"Available named {field}: {available_msg}\n"
501
+ f"Hint: Define named {field} like: {field}: [{{ {name}: 'path/to/file' }}]"
502
+ )
503
+
504
+ return name_map[name]
505
+
506
+ return SELF_REFERENCE_PATTERN.sub(replacer, text)
tasktree/types.py CHANGED
@@ -10,7 +10,10 @@ import click
10
10
 
11
11
 
12
12
  class HostnameType(click.ParamType):
13
- """Validates hostname format (not DNS resolution)."""
13
+ """
14
+ Validates hostname format (not DNS resolution).
15
+ @athena: 84a721c40458
16
+ """
14
17
 
15
18
  name = "hostname"
16
19
 
@@ -20,6 +23,9 @@ class HostnameType(click.ParamType):
20
23
  )
21
24
 
22
25
  def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
26
+ """
27
+ @athena: 8d921e52bcf2
28
+ """
23
29
  if isinstance(value, str):
24
30
  if self.HOSTNAME_PATTERN.match(value):
25
31
  return value
@@ -27,7 +33,10 @@ class HostnameType(click.ParamType):
27
33
 
28
34
 
29
35
  class EmailType(click.ParamType):
30
- """Validates email format (not deliverability)."""
36
+ """
37
+ Validates email format (not deliverability).
38
+ @athena: 95cfacc3f4cd
39
+ """
31
40
 
32
41
  name = "email"
33
42
 
@@ -37,6 +46,9 @@ class EmailType(click.ParamType):
37
46
  )
38
47
 
39
48
  def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
49
+ """
50
+ @athena: 25046aeb6e6f
51
+ """
40
52
  if isinstance(value, str):
41
53
  if self.EMAIL_PATTERN.match(value):
42
54
  return value
@@ -44,11 +56,17 @@ class EmailType(click.ParamType):
44
56
 
45
57
 
46
58
  class IPType(click.ParamType):
47
- """Validates IP address (IPv4 or IPv6)."""
59
+ """
60
+ Validates IP address (IPv4 or IPv6).
61
+ @athena: dac837bf4894
62
+ """
48
63
 
49
64
  name = "ip"
50
65
 
51
66
  def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
67
+ """
68
+ @athena: d57618e5ad89
69
+ """
52
70
  try:
53
71
  ip_address(value)
54
72
  return str(value)
@@ -57,11 +75,17 @@ class IPType(click.ParamType):
57
75
 
58
76
 
59
77
  class IPv4Type(click.ParamType):
60
- """Validates IPv4 address."""
78
+ """
79
+ Validates IPv4 address.
80
+ @athena: ea5957643fe5
81
+ """
61
82
 
62
83
  name = "ipv4"
63
84
 
64
85
  def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
86
+ """
87
+ @athena: 7ed2d17d1f1a
88
+ """
65
89
  try:
66
90
  IPv4Address(value)
67
91
  return str(value)
@@ -70,11 +94,17 @@ class IPv4Type(click.ParamType):
70
94
 
71
95
 
72
96
  class IPv6Type(click.ParamType):
73
- """Validates IPv6 address."""
97
+ """
98
+ Validates IPv6 address.
99
+ @athena: 9bc5b38d4f23
100
+ """
74
101
 
75
102
  name = "ipv6"
76
103
 
77
104
  def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
105
+ """
106
+ @athena: 4b101e4d54cf
107
+ """
78
108
  try:
79
109
  IPv6Address(value)
80
110
  return str(value)
@@ -83,11 +113,17 @@ class IPv6Type(click.ParamType):
83
113
 
84
114
 
85
115
  class DateTimeType(click.ParamType):
86
- """Validates datetime in format YYYY-MM-DDTHH:MM:SS."""
116
+ """
117
+ Validates datetime in format YYYY-MM-DDTHH:MM:SS.
118
+ @athena: c3bafa3e22e3
119
+ """
87
120
 
88
121
  name = "datetime"
89
122
 
90
123
  def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
124
+ """
125
+ @athena: 13fa66adfe94
126
+ """
91
127
  if isinstance(value, str):
92
128
  try:
93
129
  datetime.fromisoformat(value)
@@ -114,18 +150,20 @@ TYPE_MAPPING = {
114
150
 
115
151
 
116
152
  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.
153
+ """
154
+ Get Click parameter type by name with optional range constraints.
118
155
 
119
156
  Args:
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
157
+ type_name: Type name from task definition (e.g., 'str', 'int', 'hostname')
158
+ min_val: Optional minimum value for numeric types
159
+ max_val: Optional maximum value for numeric types
123
160
 
124
161
  Returns:
125
- Click parameter type instance
162
+ Click parameter type instance
126
163
 
127
164
  Raises:
128
- ValueError: If type_name is not recognized
165
+ ValueError: If type_name is not recognized
166
+ @athena: d0912868676f
129
167
  """
130
168
  if type_name not in TYPE_MAPPING:
131
169
  raise ValueError(f"Unknown type: {type_name}")