tasktree 0.0.9__py3-none-any.whl → 0.0.11__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 CHANGED
@@ -192,10 +192,12 @@ tasks:
192
192
  # deploy:
193
193
  # desc: Deploy to environment
194
194
  # deps: [build]
195
- # args: [environment, region=eu-west-1]
195
+ # args:
196
+ # - environment
197
+ # - region: { default: eu-west-1 }
196
198
  # cmd: |
197
- # echo "Deploying to {{environment}} in {{region}}"
198
- # ./deploy.sh {{environment}} {{region}}
199
+ # echo "Deploying to {{ arg.environment }} in {{ arg.region }}"
200
+ # ./deploy.sh {{ arg.environment }} {{ arg.region }}
199
201
 
200
202
  # Uncomment and modify the examples above to define your tasks
201
203
  """
tasktree/docker.py CHANGED
@@ -87,16 +87,24 @@ class DockerManager:
87
87
 
88
88
  # Build the image
89
89
  try:
90
+ docker_build_cmd = [
91
+ "docker",
92
+ "build",
93
+ "-t",
94
+ image_tag,
95
+ "-f",
96
+ str(dockerfile_path),
97
+ ]
98
+
99
+ # Add build args if environment has them (docker environments use dict for args)
100
+ if isinstance(env.args, dict):
101
+ for arg_name, arg_value in env.args.items():
102
+ docker_build_cmd.extend(["--build-arg", f"{arg_name}={arg_value}"])
103
+
104
+ docker_build_cmd.append(str(context_path))
105
+
90
106
  subprocess.run(
91
- [
92
- "docker",
93
- "build",
94
- "-t",
95
- image_tag,
96
- "-f",
97
- str(dockerfile_path),
98
- str(context_path),
99
- ],
107
+ docker_build_cmd,
100
108
  check=True,
101
109
  capture_output=False, # Show build output to user
102
110
  )
tasktree/hasher.py CHANGED
@@ -104,9 +104,16 @@ def hash_environment_definition(env) -> str:
104
104
  # Import inside function to avoid circular dependency
105
105
  from tasktree.parser import Environment
106
106
 
107
+ # Handle args - can be list (shell args) or dict (docker build args)
108
+ args_value = env.args
109
+ if isinstance(env.args, dict):
110
+ args_value = dict(sorted(env.args.items())) # Sort dict for determinism
111
+ elif isinstance(env.args, list):
112
+ args_value = sorted(env.args) # Sort list for determinism
113
+
107
114
  data = {
108
115
  "shell": env.shell,
109
- "args": sorted(env.args), # Sort for determinism
116
+ "args": args_value,
110
117
  "preamble": env.preamble,
111
118
  "dockerfile": env.dockerfile,
112
119
  "context": env.context,
tasktree/parser.py CHANGED
@@ -31,7 +31,7 @@ class Environment:
31
31
 
32
32
  name: str
33
33
  shell: str = "" # Path to shell (required for shell envs, optional for Docker)
34
- args: list[str] = field(default_factory=list)
34
+ args: list[str] | dict[str, str] = field(default_factory=list) # Shell args (list) or Docker build args (dict)
35
35
  preamble: str = ""
36
36
  # Docker-specific fields (presence of dockerfile indicates Docker environment)
37
37
  dockerfile: str = "" # Path to Dockerfile
@@ -44,7 +44,7 @@ class Environment:
44
44
  run_as_root: bool = False # If True, skip user mapping (run as root in container)
45
45
 
46
46
  def __post_init__(self):
47
- """Ensure args is always a list."""
47
+ """Ensure args is in the correct format."""
48
48
  if isinstance(self.args, str):
49
49
  self.args = [self.args]
50
50
 
@@ -60,7 +60,7 @@ class Task:
60
60
  inputs: list[str] = field(default_factory=list)
61
61
  outputs: list[str] = field(default_factory=list)
62
62
  working_dir: str = ""
63
- args: list[str] = field(default_factory=list)
63
+ args: list[str | dict[str, Any]] = field(default_factory=list) # Can be strings or dicts (each dict has single key: arg name)
64
64
  source_file: str = "" # Track which file defined this task
65
65
  env: str = "" # Environment name to use for execution
66
66
 
@@ -75,6 +75,19 @@ class Task:
75
75
  if isinstance(self.args, str):
76
76
  self.args = [self.args]
77
77
 
78
+ # Validate args is not a dict (common YAML mistake)
79
+ if isinstance(self.args, dict):
80
+ raise ValueError(
81
+ f"Task '{self.name}' has invalid 'args' syntax.\n\n"
82
+ f"Found dictionary syntax (without dashes):\n"
83
+ f" args:\n"
84
+ f" {list(self.args.keys())[0] if self.args else 'key'}: ...\n\n"
85
+ f"Correct syntax uses list format (with dashes):\n"
86
+ f" args:\n"
87
+ f" - {list(self.args.keys())[0] if self.args else 'key'}: ...\n\n"
88
+ f"Arguments must be defined as a list, not a dictionary."
89
+ )
90
+
78
91
 
79
92
  @dataclass
80
93
  class DependencyInvocation:
@@ -1246,18 +1259,16 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
1246
1259
 
1247
1260
  Supports both string format and dictionary format:
1248
1261
 
1249
- String format:
1262
+ String format (simple names only):
1250
1263
  - Simple name: "argname"
1251
1264
  - Exported (becomes env var): "$argname"
1252
- - With default: "argname=value" or "$argname=value"
1253
- - Legacy type syntax: "argname:type=value" (for backwards compat)
1254
1265
 
1255
1266
  Dictionary format:
1256
1267
  - argname: { default: "value" }
1257
1268
  - argname: { type: int, default: 42 }
1258
1269
  - argname: { type: int, min: 1, max: 100 }
1259
1270
  - argname: { type: str, choices: ["dev", "staging", "prod"] }
1260
- - $argname: { default: "value" } # Exported
1271
+ - $argname: { default: "value" } # Exported (type not allowed)
1261
1272
 
1262
1273
  Args:
1263
1274
  arg_spec: Argument specification (string or dict with single key)
@@ -1315,34 +1326,24 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
1315
1326
  if is_exported:
1316
1327
  arg_spec = arg_spec[1:] # Remove $ prefix
1317
1328
 
1318
- # Split on = to separate name:type from default
1319
- if "=" in arg_spec:
1320
- name_type, default = arg_spec.split("=", 1)
1321
- else:
1322
- name_type = arg_spec
1323
- default = None
1324
-
1325
- # Split on : to separate name from type
1326
- if ":" in name_type:
1327
- name, arg_type = name_type.split(":", 1)
1329
+ # String format only supports simple names (no = or :)
1330
+ if "=" in arg_spec or ":" in arg_spec:
1331
+ raise ValueError(
1332
+ f"Invalid argument syntax: {'$' if is_exported else ''}{arg_spec}\n\n"
1333
+ f"String format only supports simple argument names.\n"
1334
+ f"Use YAML dict format for type annotations, defaults, or constraints:\n"
1335
+ f" args:\n"
1336
+ f" - {'$' if is_exported else ''}{arg_spec.split('=')[0].split(':')[0]}: {{ default: value }}"
1337
+ )
1328
1338
 
1329
- # Exported arguments cannot have type annotations
1330
- if is_exported:
1331
- raise ValueError(
1332
- f"Type annotations not allowed on exported arguments\n"
1333
- f"In argument: ${name}:{arg_type}\n\n"
1334
- f"Exported arguments are always strings. Remove the type annotation:\n"
1335
- f" args: [${name}]"
1336
- )
1337
- else:
1338
- name = name_type
1339
- arg_type = "str"
1339
+ name = arg_spec
1340
+ arg_type = "str"
1340
1341
 
1341
- # String format doesn't support min/max/choices
1342
+ # String format doesn't support min/max/choices/defaults
1342
1343
  return ArgSpec(
1343
1344
  name=name,
1344
1345
  arg_type=arg_type,
1345
- default=default,
1346
+ default=None,
1346
1347
  is_exported=is_exported,
1347
1348
  min_val=None,
1348
1349
  max_val=None,
@@ -1390,6 +1391,15 @@ def _parse_arg_dict(arg_name: str, config: dict, is_exported: bool) -> ArgSpec:
1390
1391
  f"Exported arguments are always strings. Remove the 'type' field"
1391
1392
  )
1392
1393
 
1394
+ # Exported arguments must have string defaults (if any default is provided)
1395
+ if is_exported and default is not None and not isinstance(default, str):
1396
+ raise ValueError(
1397
+ f"Exported argument '${arg_name}' must have a string default value.\n"
1398
+ f"Got: {default!r} (type: {type(default).__name__})\n"
1399
+ f"Exported arguments become environment variables, which are always strings.\n"
1400
+ f"Use a quoted string: ${arg_name}: {{ default: \"{default}\" }}"
1401
+ )
1402
+
1393
1403
  # Validate choices
1394
1404
  if choices is not None:
1395
1405
  # Validate choices is a list
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.9
3
+ Version: 0.0.11
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -604,7 +604,9 @@ Dependencies can invoke tasks with specific arguments, enabling flexible and reu
604
604
  tasks:
605
605
  # Task with parameters
606
606
  process:
607
- args: [mode, verbose=false]
607
+ args:
608
+ - mode
609
+ - verbose: { default: false }
608
610
  cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
609
611
 
610
612
  # Simple dependency (uses defaults)
@@ -0,0 +1,15 @@
1
+ tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
2
+ tasktree/cli.py,sha256=uL4RGap1U7-_4mcdEGbsELR4cvm1aUaqbvnX8XJFNKc,17652
3
+ tasktree/docker.py,sha256=qvja8G63uAcC73YMVY739egda1_CcBtoqzm0qIJU_Q8,14443
4
+ tasktree/executor.py,sha256=Q7Bks5B88i-IyZDpxGSps9MM3uflz0U3yn4Rtq_uHMM,42266
5
+ tasktree/graph.py,sha256=oXLxX0Ix4zSkVBg8_3x9K7WxSFpg136sp4MF-d2mDEQ,9682
6
+ tasktree/hasher.py,sha256=0GrnCfwAXnwq_kpnHFFb12B5_2VFNXx6Ng7hTdcCyXo,4415
7
+ tasktree/parser.py,sha256=rHJuYMM4AUjM1E-Jh3SpUpBRKGkciYOvgfgERoXylSE,67364
8
+ tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
9
+ tasktree/substitution.py,sha256=M_qcP0NKJATrKcNShSqHJatneuth1RVwTk1ci8-ZuxQ,6473
10
+ tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
11
+ tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
12
+ tasktree-0.0.11.dist-info/METADATA,sha256=a41OmLVRm4BbIpZT4e7v-l3g3gmw3DD1Eg0OXnrVsYA,37151
13
+ tasktree-0.0.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ tasktree-0.0.11.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
15
+ tasktree-0.0.11.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
2
- tasktree/cli.py,sha256=H5T8wOxLBGx-ZTQEnkoJrX3srgD5b_7BLf1IWl18M2M,17597
3
- tasktree/docker.py,sha256=R69NcZw4MyaxEXyJAwniYCm877iaI10jRhxlLmkA6Fs,14119
4
- tasktree/executor.py,sha256=Q7Bks5B88i-IyZDpxGSps9MM3uflz0U3yn4Rtq_uHMM,42266
5
- tasktree/graph.py,sha256=oXLxX0Ix4zSkVBg8_3x9K7WxSFpg136sp4MF-d2mDEQ,9682
6
- tasktree/hasher.py,sha256=C8oN-K6dtL3vLHSPhKR7uu5c1d4vplGSAMZUq5M4scw,4125
7
- tasktree/parser.py,sha256=DjdfsKErdBggqS8Tw_mwuMvMSavIJqq2BCdsh1O82CY,66333
8
- tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
9
- tasktree/substitution.py,sha256=M_qcP0NKJATrKcNShSqHJatneuth1RVwTk1ci8-ZuxQ,6473
10
- tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
11
- tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
12
- tasktree-0.0.9.dist-info/METADATA,sha256=VBgQ1ZF2hacw1CajhxcjkrGTyygnf-uWxiZm7H92AyE,37123
13
- tasktree-0.0.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- tasktree-0.0.9.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
15
- tasktree-0.0.9.dist-info/RECORD,,