tasktree 0.0.20__py3-none-any.whl → 0.0.22__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/types.py CHANGED
@@ -3,14 +3,16 @@
3
3
  import re
4
4
  from datetime import datetime
5
5
  from ipaddress import IPv4Address, IPv6Address, ip_address
6
- from pathlib import Path
7
6
  from typing import Any, Optional
8
7
 
9
8
  import click
10
9
 
11
10
 
12
11
  class HostnameType(click.ParamType):
13
- """Validates hostname format (not DNS resolution)."""
12
+ """
13
+ Validates hostname format (not DNS resolution).
14
+ @athena: 84a721c40458
15
+ """
14
16
 
15
17
  name = "hostname"
16
18
 
@@ -19,7 +21,12 @@ class HostnameType(click.ParamType):
19
21
  r"^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.[A-Za-z0-9-]{1,63})*\.?$"
20
22
  )
21
23
 
22
- def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
24
+ def convert(
25
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
26
+ ) -> str:
27
+ """
28
+ @athena: 8d921e52bcf2
29
+ """
23
30
  if isinstance(value, str):
24
31
  if self.HOSTNAME_PATTERN.match(value):
25
32
  return value
@@ -27,16 +34,22 @@ class HostnameType(click.ParamType):
27
34
 
28
35
 
29
36
  class EmailType(click.ParamType):
30
- """Validates email format (not deliverability)."""
37
+ """
38
+ Validates email format (not deliverability).
39
+ @athena: 95cfacc3f4cd
40
+ """
31
41
 
32
42
  name = "email"
33
43
 
34
44
  # Basic email validation (RFC 5322 simplified)
35
- EMAIL_PATTERN = re.compile(
36
- r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
37
- )
38
-
39
- def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
45
+ EMAIL_PATTERN = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
46
+
47
+ def convert(
48
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
49
+ ) -> str:
50
+ """
51
+ @athena: 25046aeb6e6f
52
+ """
40
53
  if isinstance(value, str):
41
54
  if self.EMAIL_PATTERN.match(value):
42
55
  return value
@@ -44,11 +57,19 @@ class EmailType(click.ParamType):
44
57
 
45
58
 
46
59
  class IPType(click.ParamType):
47
- """Validates IP address (IPv4 or IPv6)."""
60
+ """
61
+ Validates IP address (IPv4 or IPv6).
62
+ @athena: dac837bf4894
63
+ """
48
64
 
49
65
  name = "ip"
50
66
 
51
- def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
67
+ def convert(
68
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
69
+ ) -> str:
70
+ """
71
+ @athena: d57618e5ad89
72
+ """
52
73
  try:
53
74
  ip_address(value)
54
75
  return str(value)
@@ -57,11 +78,19 @@ class IPType(click.ParamType):
57
78
 
58
79
 
59
80
  class IPv4Type(click.ParamType):
60
- """Validates IPv4 address."""
81
+ """
82
+ Validates IPv4 address.
83
+ @athena: ea5957643fe5
84
+ """
61
85
 
62
86
  name = "ipv4"
63
87
 
64
- def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
88
+ def convert(
89
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
90
+ ) -> str:
91
+ """
92
+ @athena: 7ed2d17d1f1a
93
+ """
65
94
  try:
66
95
  IPv4Address(value)
67
96
  return str(value)
@@ -70,11 +99,19 @@ class IPv4Type(click.ParamType):
70
99
 
71
100
 
72
101
  class IPv6Type(click.ParamType):
73
- """Validates IPv6 address."""
102
+ """
103
+ Validates IPv6 address.
104
+ @athena: 9bc5b38d4f23
105
+ """
74
106
 
75
107
  name = "ipv6"
76
108
 
77
- def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
109
+ def convert(
110
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
111
+ ) -> str:
112
+ """
113
+ @athena: 4b101e4d54cf
114
+ """
78
115
  try:
79
116
  IPv6Address(value)
80
117
  return str(value)
@@ -83,18 +120,30 @@ class IPv6Type(click.ParamType):
83
120
 
84
121
 
85
122
  class DateTimeType(click.ParamType):
86
- """Validates datetime in format YYYY-MM-DDTHH:MM:SS."""
123
+ """
124
+ Validates datetime in format YYYY-MM-DDTHH:MM:SS.
125
+ @athena: c3bafa3e22e3
126
+ """
87
127
 
88
128
  name = "datetime"
89
129
 
90
- def convert(self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]) -> str:
130
+ def convert(
131
+ self, value: Any, param: Optional[click.Parameter], ctx: Optional[click.Context]
132
+ ) -> str:
133
+ """
134
+ @athena: 13fa66adfe94
135
+ """
91
136
  if isinstance(value, str):
92
137
  try:
93
138
  datetime.fromisoformat(value)
94
139
  return value
95
140
  except ValueError:
96
141
  pass
97
- self.fail(f"{value!r} is not a valid datetime (expected YYYY-MM-DDTHH:MM:SS format)", param, ctx)
142
+ self.fail(
143
+ f"{value!r} is not a valid datetime (expected YYYY-MM-DDTHH:MM:SS format)",
144
+ param,
145
+ ctx,
146
+ )
98
147
 
99
148
 
100
149
  # Type registry for dynamic parameter creation
@@ -113,19 +162,25 @@ TYPE_MAPPING = {
113
162
  }
114
163
 
115
164
 
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.
165
+ def get_click_type(
166
+ type_name: str,
167
+ min_val: int | float | None = None,
168
+ max_val: int | float | None = None,
169
+ ) -> click.ParamType:
170
+ """
171
+ Get Click parameter type by name with optional range constraints.
118
172
 
119
173
  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
174
+ type_name: Type name from task definition (e.g., 'str', 'int', 'hostname')
175
+ min_val: Optional minimum value for numeric types
176
+ max_val: Optional maximum value for numeric types
123
177
 
124
178
  Returns:
125
- Click parameter type instance
179
+ Click parameter type instance
126
180
 
127
181
  Raises:
128
- ValueError: If type_name is not recognized
182
+ ValueError: If type_name is not recognized
183
+ @athena: d0912868676f
129
184
  """
130
185
  if type_name not in TYPE_MAPPING:
131
186
  raise ValueError(f"Unknown type: {type_name}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.20
3
+ Version: 0.0.22
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -10,7 +10,9 @@ Requires-Dist: pyyaml>=6.0
10
10
  Requires-Dist: rich>=13.0.0
11
11
  Requires-Dist: typer>=0.9.0
12
12
  Provides-Extra: dev
13
+ Requires-Dist: black>=26.1.0; extra == 'dev'
13
14
  Requires-Dist: pytest>=9.0.2; extra == 'dev'
15
+ Requires-Dist: ruff>=0.14.14; extra == 'dev'
14
16
  Description-Content-Type: text/markdown
15
17
 
16
18
  # Task Tree (tt)
@@ -255,20 +257,19 @@ tasks:
255
257
  cmd: go build -o dist/binary # Command to execute
256
258
  ```
257
259
 
260
+ **Task name constraints:**
261
+ - Task names cannot contain dots (`.`) - they are reserved for namespacing imported tasks
262
+ - Example: `build.release` is invalid as a task name, but valid as a reference to task `release` in namespace `build`
263
+
258
264
  ### Commands
259
265
 
260
- **Single-line commands** are executed directly via the configured shell:
266
+ All commands are executed by writing them to temporary script files. This provides consistent behavior and better shell syntax support:
261
267
 
262
268
  ```yaml
263
269
  tasks:
264
270
  build:
265
271
  cmd: cargo build --release
266
- ```
267
-
268
- **Multi-line commands** are written to temporary script files for proper execution:
269
272
 
270
- ```yaml
271
- tasks:
272
273
  deploy:
273
274
  cmd: |
274
275
  mkdir -p dist
@@ -276,7 +277,7 @@ tasks:
276
277
  rsync -av dist/ server:/opt/app/
277
278
  ```
278
279
 
279
- Multi-line commands preserve shell syntax (line continuations, heredocs, etc.) and support shebangs on Unix/macOS.
280
+ Commands preserve shell syntax (line continuations, heredocs, etc.) and support shebangs on Unix/macOS.
280
281
 
281
282
  Or use folded blocks for long single-line commands:
282
283
 
@@ -292,7 +293,7 @@ tasks:
292
293
 
293
294
  ### Execution Environments
294
295
 
295
- Configure custom shell environments for task execution:
296
+ Configure custom shell environments for task execution. Use the `preamble` field to add initialization code to all commands:
296
297
 
297
298
  ```yaml
298
299
  environments:
@@ -300,17 +301,14 @@ environments:
300
301
 
301
302
  bash-strict:
302
303
  shell: bash
303
- args: ['-c'] # For single-line: bash -c "command"
304
- preamble: | # For multi-line: prepended to script
304
+ preamble: | # Prepended to all commands
305
305
  set -euo pipefail
306
306
 
307
307
  python:
308
308
  shell: python
309
- args: ['-c']
310
309
 
311
310
  powershell:
312
311
  shell: powershell
313
- args: ['-ExecutionPolicy', 'Bypass', '-Command']
314
312
  preamble: |
315
313
  $ErrorActionPreference = 'Stop'
316
314
 
@@ -339,8 +337,8 @@ tasks:
339
337
  4. Platform default (bash on Unix, cmd on Windows)
340
338
 
341
339
  **Platform defaults** when no environments are configured:
342
- - **Unix/macOS**: bash with `-c` args
343
- - **Windows**: cmd with `/c` args
340
+ - **Unix/macOS**: bash
341
+ - **Windows**: cmd
344
342
 
345
343
  ### Docker Environments
346
344
 
@@ -835,7 +833,7 @@ Hint: Define named outputs like: outputs: [{ missing: 'path/to/file' }]
835
833
 
836
834
  ### Self-References
837
835
 
838
- Tasks can reference their own named inputs and outputs using `{{ self.inputs.name }}` and `{{ self.outputs.name }}` templates. This eliminates repetition when paths contain variables or when tasks have multiple inputs/outputs.
836
+ Tasks can reference their own inputs and outputs using `{{ self.inputs.name }}` (named access) or `{{ self.inputs.0 }}` (positional access) templates. This eliminates repetition when paths contain variables or when tasks have multiple inputs/outputs.
839
837
 
840
838
  **Named Inputs and Outputs:**
841
839
 
@@ -862,8 +860,10 @@ tasks:
862
860
 
863
861
  - **Defining named inputs**: `inputs: [{ name: "path/to/file" }]`
864
862
  - **Defining named outputs**: `outputs: [{ name: "path/to/file" }]`
865
- - **Referencing inputs**: `{{ self.inputs.input_name }}`
866
- - **Referencing outputs**: `{{ self.outputs.output_name }}`
863
+ - **Defining anonymous inputs**: `inputs: ["path/to/file"]`
864
+ - **Defining anonymous outputs**: `outputs: ["path/to/file"]`
865
+ - **Referencing by name**: `{{ self.inputs.input_name }}` or `{{ self.outputs.output_name }}`
866
+ - **Referencing by index**: `{{ self.inputs.0 }}` or `{{ self.outputs.1 }}` (0-based)
867
867
  - **Mixed format**: Can combine named and anonymous inputs/outputs in the same task
868
868
 
869
869
  **Why Use Self-References?**
@@ -972,6 +972,129 @@ tasks:
972
972
  cmd: build-tool --config {{ self.inputs.config }} --output {{ self.outputs.binary }}
973
973
  ```
974
974
 
975
+ **Positional Index References:**
976
+
977
+ In addition to named references, you can access inputs and outputs by their positional index using `{{ self.inputs.0 }}`, `{{ self.inputs.1 }}`, etc. This provides an alternative way to reference items, especially useful for:
978
+
979
+ - **Anonymous inputs/outputs**: Reference items that don't have names
980
+ - **Simple sequential access**: When order is more important than naming
981
+ - **Mixed with named access**: Use both styles in the same task
982
+
983
+ **Syntax:**
984
+
985
+ ```yaml
986
+ tasks:
987
+ process:
988
+ inputs: ["file1.txt", "file2.txt", "file3.txt"]
989
+ outputs: ["output1.txt", "output2.txt"]
990
+ cmd: |
991
+ cat {{ self.inputs.0 }} {{ self.inputs.1 }} > {{ self.outputs.0 }}
992
+ cat {{ self.inputs.2 }} > {{ self.outputs.1 }}
993
+ ```
994
+
995
+ Indices follow YAML declaration order, starting from 0 (zero-based indexing):
996
+ - First input/output = index 0
997
+ - Second input/output = index 1
998
+ - Third input/output = index 2, etc.
999
+
1000
+ **Works with Both Named and Anonymous:**
1001
+
1002
+ ```yaml
1003
+ tasks:
1004
+ build:
1005
+ inputs:
1006
+ - config: "build.yaml" # Index 0, also accessible as {{ self.inputs.config }}
1007
+ - "src/**/*.c" # Index 1, ONLY accessible as {{ self.inputs.1 }}
1008
+ - headers: "include/*.h" # Index 2, also accessible as {{ self.inputs.headers }}
1009
+ outputs:
1010
+ - "dist/app.js" # Index 0
1011
+ - bundle: "dist/bundle.js" # Index 1, also accessible as {{ self.outputs.bundle }}
1012
+ cmd: |
1013
+ # Mix positional and named references
1014
+ build-tool \
1015
+ --config {{ self.inputs.0 }} \
1016
+ --sources {{ self.inputs.1 }} \
1017
+ --headers {{ self.inputs.headers }} \
1018
+ --output {{ self.outputs.bundle }}
1019
+ ```
1020
+
1021
+ **Same Item, Multiple Ways:**
1022
+
1023
+ Named items can be accessed by both name and index:
1024
+
1025
+ ```yaml
1026
+ tasks:
1027
+ copy:
1028
+ inputs:
1029
+ - source: data.txt
1030
+ cmd: |
1031
+ # These are equivalent:
1032
+ cat {{ self.inputs.source }} > copy1.txt
1033
+ cat {{ self.inputs.0 }} > copy2.txt
1034
+ ```
1035
+
1036
+ **With Variables and Arguments:**
1037
+
1038
+ Positional references work with variable-expanded paths:
1039
+
1040
+ ```yaml
1041
+ variables:
1042
+ version: "1.0"
1043
+
1044
+ tasks:
1045
+ package:
1046
+ args: [platform]
1047
+ inputs:
1048
+ - "dist/app-{{ var.version }}.js"
1049
+ - "dist/lib-{{ arg.platform }}.so"
1050
+ outputs: ["release-{{ var.version }}-{{ arg.platform }}.tar.gz"]
1051
+ cmd: tar czf {{ self.outputs.0 }} {{ self.inputs.0 }} {{ self.inputs.1 }}
1052
+ ```
1053
+
1054
+ **Index Boundaries:**
1055
+
1056
+ Indices are validated before execution:
1057
+
1058
+ ```yaml
1059
+ tasks:
1060
+ build:
1061
+ inputs: ["file1.txt", "file2.txt"]
1062
+ cmd: cat {{ self.inputs.5 }} # Error: index 5 out of bounds!
1063
+ ```
1064
+
1065
+ Error message:
1066
+ ```
1067
+ Task 'build' references input index '5' but only has 2 inputs (indices 0-1)
1068
+ ```
1069
+
1070
+ **Empty Lists:**
1071
+
1072
+ Referencing an index when no inputs/outputs exist:
1073
+
1074
+ ```yaml
1075
+ tasks:
1076
+ generate:
1077
+ cmd: echo "test" > {{ self.outputs.0 }} # Error: no outputs defined!
1078
+ ```
1079
+
1080
+ Error message:
1081
+ ```
1082
+ Task 'generate' references output index '0' but has no outputs defined
1083
+ ```
1084
+
1085
+ **When to Use Index References:**
1086
+
1087
+ - **Anonymous items**: Only way to reference inputs/outputs without names
1088
+ - **Order-based processing**: When the sequence matters more than naming
1089
+ - **Simple tasks**: Quick access without defining names
1090
+ - **Compatibility**: Accessing items in legacy YAML that uses anonymous format
1091
+
1092
+ **When to Use Named References:**
1093
+
1094
+ - **Clarity**: Names make commands more readable (`{{ self.inputs.config }}` vs `{{ self.inputs.2 }}`)
1095
+ - **Maintainability**: Adding/removing items doesn't break indices
1096
+ - **Complex tasks**: Many inputs/outputs are easier to manage with names
1097
+
975
1098
  **Error Messages:**
976
1099
 
977
1100
  If you reference a non-existent input or output:
@@ -1011,18 +1134,21 @@ Hint: Define named inputs like: inputs: [{ src: 'file.txt' }]
1011
1134
 
1012
1135
  **Key Behaviors:**
1013
1136
 
1137
+ - **Two access methods**: Reference by name (`{{ self.inputs.name }}`) or by index (`{{ self.inputs.0 }}`)
1014
1138
  - **Template resolution**: Self-references are resolved during dependency graph planning
1015
1139
  - **Substitution order**: Variables → Dependency outputs → Self-references → Arguments/Environment
1016
- - **Fail-fast validation**: Errors are caught before execution begins
1017
- - **Clear error messages**: Lists available names if reference doesn't exist
1140
+ - **Fail-fast validation**: Errors are caught before execution begins (missing names, out-of-bounds indices)
1141
+ - **Clear error messages**: Lists available names/indices if reference doesn't exist
1018
1142
  - **Backward compatible**: Existing anonymous inputs/outputs work unchanged
1019
1143
  - **State tracking**: Works correctly with incremental execution and freshness checks
1144
+ - **Index order**: Positional indices follow YAML declaration order (0-based)
1020
1145
 
1021
1146
  **Limitations:**
1022
1147
 
1023
- - **Anonymous not referenceable**: Only named inputs/outputs can be referenced
1148
+ - **Anonymous not referenceable by name**: Anonymous inputs/outputs cannot be referenced by name (use positional index instead: `{{ self.inputs.0 }}`)
1024
1149
  - **Case sensitive**: `{{ self.inputs.Src }}` and `{{ self.inputs.src }}` are different
1025
1150
  - **Argument defaults**: Self-references in argument defaults are not supported (arguments are evaluated before self-references)
1151
+ - **No negative indices**: Python-style negative indexing (`{{ self.inputs.-1 }}`) is not supported
1026
1152
 
1027
1153
  **Use Cases:**
1028
1154
 
@@ -0,0 +1,14 @@
1
+ tasktree/__init__.py,sha256=ZfI6vy-TxinN7eK0_XjfHUFq83pFV1XiHFrai_SHNqw,1251
2
+ tasktree/cli.py,sha256=588ngMB27T6FqUp5LIJ_x_UNoPRlMv39b1_U0a88tfE,23577
3
+ tasktree/docker.py,sha256=DAvdr6kZqkhx8J9fhsz26Z41hFPreANC60vdunvPX00,15050
4
+ tasktree/executor.py,sha256=UF2H-qCgkJ5E2QoYt3To3a-ypwTIYJT9oyWRcBw2CzQ,46800
5
+ tasktree/graph.py,sha256=Ya4ATQidj0rlHxvvbWF0Xx2J18MTpz2XPAHTcJXFcBg,23637
6
+ tasktree/hasher.py,sha256=fvs7vNBrHdAA6pX-ZLhNnWLtd5qdxjAVFJC8SDF-DhQ,6730
7
+ tasktree/parser.py,sha256=OUuME_ZqITAMQEL90w-yJFLKoEquF_IIa-By8MifMvI,101074
8
+ tasktree/state.py,sha256=R4ZXgfs_rwrOH9nEb4mOjuHa7gAVqi7QPVCi0CouNQA,3961
9
+ tasktree/substitution.py,sha256=hwox1m9r8p5b0Fcln12C63GexJHc5ZMUoqOrpqQLuPk,18663
10
+ tasktree/types.py,sha256=nCiPrxg3r3ZX4CoZ6L5FrkJO9mgz_lFsZ-98X8tYgns,5015
11
+ tasktree-0.0.22.dist-info/METADATA,sha256=fuvZWDbtlwfbQNOP_TMCDRSDGKdZYUTeROo3eqVcJ6o,54519
12
+ tasktree-0.0.22.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ tasktree-0.0.22.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
14
+ tasktree-0.0.22.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- tasktree/__init__.py,sha256=m7fLsPUft99oB_XXr4dOu2yUWu74zVutkw1-3zGrG5Y,1227
2
- tasktree/cli.py,sha256=S5ypqQlvCxdAvlBfO8TJZhvMoc086wqgvmOm8220678,21220
3
- tasktree/docker.py,sha256=qvja8G63uAcC73YMVY739egda1_CcBtoqzm0qIJU_Q8,14443
4
- tasktree/executor.py,sha256=g4mHtoO3wVIxyqNALIdJOEwlEqRkSY0eY-6sl2jF-IA,46463
5
- tasktree/graph.py,sha256=T78JH0whP7VquEvtOVN-8ePyHNcseTQoEouijDrgmkw,22663
6
- tasktree/hasher.py,sha256=o7Akd_AgGkAsnv9biK0AcbhlcqUQ9ne5y_6r4zoFaw0,5493
7
- tasktree/parser.py,sha256=R0swEkKBPGeijLHxD1CbQjtoKVn2gRJadsZuyKj1sdM,97922
8
- tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
9
- tasktree/substitution.py,sha256=FhtFI0ciK9bQxPdORvpf1coa59XxizKiBiUwHJp0PtI,16811
10
- tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
11
- tasktree-0.0.20.dist-info/METADATA,sha256=C8cdZtyz-dgiVoN1H2hy7wQNAA-K784WYFfqlZEOXnA,50237
12
- tasktree-0.0.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
- tasktree-0.0.20.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
14
- tasktree-0.0.20.dist-info/RECORD,,