tasktree 0.0.10__py3-none-any.whl → 0.0.12__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/parser.py CHANGED
@@ -287,26 +287,35 @@ def _is_env_variable_reference(value: Any) -> bool:
287
287
  return isinstance(value, dict) and "env" in value
288
288
 
289
289
 
290
- def _validate_env_variable_reference(var_name: str, value: dict) -> str:
291
- """Validate and extract environment variable name from reference.
290
+ def _validate_env_variable_reference(var_name: str, value: dict) -> tuple[str, str | None]:
291
+ """Validate and extract environment variable name and optional default from reference.
292
292
 
293
293
  Args:
294
294
  var_name: Name of the variable being defined
295
- value: Dict that should be { env: ENV_VAR_NAME }
295
+ value: Dict that should be { env: ENV_VAR_NAME } or { env: ENV_VAR_NAME, default: "value" }
296
296
 
297
297
  Returns:
298
- Environment variable name
298
+ Tuple of (environment variable name, default value or None)
299
299
 
300
300
  Raises:
301
301
  ValueError: If reference is invalid
302
302
  """
303
- # Validate dict structure
304
- if len(value) != 1:
305
- extra_keys = [k for k in value.keys() if k != "env"]
303
+ # Validate dict structure - allow 'env' and optionally 'default'
304
+ valid_keys = {"env", "default"}
305
+ invalid_keys = set(value.keys()) - valid_keys
306
+ if invalid_keys:
306
307
  raise ValueError(
307
308
  f"Invalid environment variable reference in variable '{var_name}'.\n"
308
- f"Expected: {{ env: VARIABLE_NAME }}\n"
309
- f"Found extra keys: {', '.join(extra_keys)}"
309
+ f"Expected: {{ env: VARIABLE_NAME }} or {{ env: VARIABLE_NAME, default: \"value\" }}\n"
310
+ f"Found invalid keys: {', '.join(invalid_keys)}"
311
+ )
312
+
313
+ # Validate 'env' key is present
314
+ if "env" not in value:
315
+ raise ValueError(
316
+ f"Invalid environment variable reference in variable '{var_name}'.\n"
317
+ f"Missing required 'env' key.\n"
318
+ f"Expected: {{ env: VARIABLE_NAME }} or {{ env: VARIABLE_NAME, default: \"value\" }}"
310
319
  )
311
320
 
312
321
  env_var_name = value["env"]
@@ -315,7 +324,7 @@ def _validate_env_variable_reference(var_name: str, value: dict) -> str:
315
324
  if not env_var_name or not isinstance(env_var_name, str):
316
325
  raise ValueError(
317
326
  f"Invalid environment variable reference in variable '{var_name}'.\n"
318
- f"Expected: {{ env: VARIABLE_NAME }}\n"
327
+ f"Expected: {{ env: VARIABLE_NAME }} or {{ env: VARIABLE_NAME, default: \"value\" }}"
319
328
  f"Found: {{ env: {env_var_name!r} }}"
320
329
  )
321
330
 
@@ -327,23 +336,36 @@ def _validate_env_variable_reference(var_name: str, value: dict) -> str:
327
336
  f"and contain only alphanumerics and underscores."
328
337
  )
329
338
 
330
- return env_var_name
339
+ # Extract and validate default if present
340
+ default = value.get("default")
341
+ if default is not None:
342
+ # Default must be a string (env vars are always strings)
343
+ if not isinstance(default, str):
344
+ raise ValueError(
345
+ f"Invalid default value in variable '{var_name}'.\n"
346
+ f"Environment variable defaults must be strings.\n"
347
+ f"Got: {default!r} (type: {type(default).__name__})\n"
348
+ f"Use a quoted string: {{ env: {env_var_name}, default: \"{default}\" }}"
349
+ )
350
+
351
+ return env_var_name, default
331
352
 
332
353
 
333
- def _resolve_env_variable(var_name: str, env_var_name: str) -> str:
354
+ def _resolve_env_variable(var_name: str, env_var_name: str, default: str | None = None) -> str:
334
355
  """Resolve environment variable value.
335
356
 
336
357
  Args:
337
358
  var_name: Name of the variable being defined
338
359
  env_var_name: Name of environment variable to read
360
+ default: Optional default value to use if environment variable is not set
339
361
 
340
362
  Returns:
341
- Environment variable value as string
363
+ Environment variable value as string, or default if not set and default provided
342
364
 
343
365
  Raises:
344
- ValueError: If environment variable is not set
366
+ ValueError: If environment variable is not set and no default provided
345
367
  """
346
- value = os.environ.get(env_var_name)
368
+ value = os.environ.get(env_var_name, default)
347
369
 
348
370
  if value is None:
349
371
  raise ValueError(
@@ -730,11 +752,11 @@ def _resolve_variable_value(
730
752
 
731
753
  # Check if this is an environment variable reference
732
754
  if _is_env_variable_reference(raw_value):
733
- # Validate and extract env var name
734
- env_var_name = _validate_env_variable_reference(name, raw_value)
755
+ # Validate and extract env var name and optional default
756
+ env_var_name, default = _validate_env_variable_reference(name, raw_value)
735
757
 
736
- # Resolve from os.environ
737
- string_value = _resolve_env_variable(name, env_var_name)
758
+ # Resolve from os.environ (with optional default)
759
+ string_value = _resolve_env_variable(name, env_var_name, default)
738
760
 
739
761
  # Still perform variable-in-variable substitution
740
762
  from tasktree.substitution import substitute_variables
@@ -1259,18 +1281,16 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
1259
1281
 
1260
1282
  Supports both string format and dictionary format:
1261
1283
 
1262
- String format:
1284
+ String format (simple names only):
1263
1285
  - Simple name: "argname"
1264
1286
  - Exported (becomes env var): "$argname"
1265
- - With default: "argname=value" or "$argname=value"
1266
- - Legacy type syntax: "argname:type=value" (for backwards compat)
1267
1287
 
1268
1288
  Dictionary format:
1269
1289
  - argname: { default: "value" }
1270
1290
  - argname: { type: int, default: 42 }
1271
1291
  - argname: { type: int, min: 1, max: 100 }
1272
1292
  - argname: { type: str, choices: ["dev", "staging", "prod"] }
1273
- - $argname: { default: "value" } # Exported
1293
+ - $argname: { default: "value" } # Exported (type not allowed)
1274
1294
 
1275
1295
  Args:
1276
1296
  arg_spec: Argument specification (string or dict with single key)
@@ -1328,34 +1348,24 @@ def parse_arg_spec(arg_spec: str | dict) -> ArgSpec:
1328
1348
  if is_exported:
1329
1349
  arg_spec = arg_spec[1:] # Remove $ prefix
1330
1350
 
1331
- # Split on = to separate name:type from default
1332
- if "=" in arg_spec:
1333
- name_type, default = arg_spec.split("=", 1)
1334
- else:
1335
- name_type = arg_spec
1336
- default = None
1351
+ # String format only supports simple names (no = or :)
1352
+ if "=" in arg_spec or ":" in arg_spec:
1353
+ raise ValueError(
1354
+ f"Invalid argument syntax: {'$' if is_exported else ''}{arg_spec}\n\n"
1355
+ f"String format only supports simple argument names.\n"
1356
+ f"Use YAML dict format for type annotations, defaults, or constraints:\n"
1357
+ f" args:\n"
1358
+ f" - {'$' if is_exported else ''}{arg_spec.split('=')[0].split(':')[0]}: {{ default: value }}"
1359
+ )
1337
1360
 
1338
- # Split on : to separate name from type
1339
- if ":" in name_type:
1340
- name, arg_type = name_type.split(":", 1)
1361
+ name = arg_spec
1362
+ arg_type = "str"
1341
1363
 
1342
- # Exported arguments cannot have type annotations
1343
- if is_exported:
1344
- raise ValueError(
1345
- f"Type annotations not allowed on exported arguments\n"
1346
- f"In argument: ${name}:{arg_type}\n\n"
1347
- f"Exported arguments are always strings. Remove the type annotation:\n"
1348
- f" args: [${name}]"
1349
- )
1350
- else:
1351
- name = name_type
1352
- arg_type = "str"
1353
-
1354
- # String format doesn't support min/max/choices
1364
+ # String format doesn't support min/max/choices/defaults
1355
1365
  return ArgSpec(
1356
1366
  name=name,
1357
1367
  arg_type=arg_type,
1358
- default=default,
1368
+ default=None,
1359
1369
  is_exported=is_exported,
1360
1370
  min_val=None,
1361
1371
  max_val=None,
@@ -1403,6 +1413,15 @@ def _parse_arg_dict(arg_name: str, config: dict, is_exported: bool) -> ArgSpec:
1403
1413
  f"Exported arguments are always strings. Remove the 'type' field"
1404
1414
  )
1405
1415
 
1416
+ # Exported arguments must have string defaults (if any default is provided)
1417
+ if is_exported and default is not None and not isinstance(default, str):
1418
+ raise ValueError(
1419
+ f"Exported argument '${arg_name}' must have a string default value.\n"
1420
+ f"Got: {default!r} (type: {type(default).__name__})\n"
1421
+ f"Exported arguments become environment variables, which are always strings.\n"
1422
+ f"Use a quoted string: ${arg_name}: {{ default: \"{default}\" }}"
1423
+ )
1424
+
1406
1425
  # Validate choices
1407
1426
  if choices is not None:
1408
1427
  # Validate choices is a list
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.10
3
+ Version: 0.0.12
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)
@@ -716,10 +718,13 @@ For more complex scenarios, define environment variables in the `variables` sect
716
718
 
717
719
  ```yaml
718
720
  variables:
719
- # Direct env reference (resolved at parse time)
721
+ # Required env var (error if not set)
720
722
  api_key: { env: API_KEY }
721
- db_host: { env: DATABASE_HOST }
722
-
723
+
724
+ # Optional env var with default
725
+ port: { env: PORT, default: "8080" }
726
+ log_level: { env: LOG_LEVEL, default: "info" }
727
+
723
728
  # Or using string substitution
724
729
  deploy_user: "{{ env.DEPLOY_USER }}"
725
730
 
@@ -1,15 +1,15 @@
1
1
  tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
2
- tasktree/cli.py,sha256=H5T8wOxLBGx-ZTQEnkoJrX3srgD5b_7BLf1IWl18M2M,17597
2
+ tasktree/cli.py,sha256=uL4RGap1U7-_4mcdEGbsELR4cvm1aUaqbvnX8XJFNKc,17652
3
3
  tasktree/docker.py,sha256=qvja8G63uAcC73YMVY739egda1_CcBtoqzm0qIJU_Q8,14443
4
4
  tasktree/executor.py,sha256=Q7Bks5B88i-IyZDpxGSps9MM3uflz0U3yn4Rtq_uHMM,42266
5
5
  tasktree/graph.py,sha256=oXLxX0Ix4zSkVBg8_3x9K7WxSFpg136sp4MF-d2mDEQ,9682
6
6
  tasktree/hasher.py,sha256=0GrnCfwAXnwq_kpnHFFb12B5_2VFNXx6Ng7hTdcCyXo,4415
7
- tasktree/parser.py,sha256=N_dXHl5UF0rBvIVdbsVZOo5Ur5uFEKgY1sSPNfScTxc,67135
7
+ tasktree/parser.py,sha256=XdFuELqrrhHc45HeMpo6-gflopZM7kYTqO1lQcFwtFk,68782
8
8
  tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
9
9
  tasktree/substitution.py,sha256=M_qcP0NKJATrKcNShSqHJatneuth1RVwTk1ci8-ZuxQ,6473
10
10
  tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
11
11
  tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
12
- tasktree-0.0.10.dist-info/METADATA,sha256=JyhF89pfUwr0bkV33rNae-0ytPyOEr2bKHfiWN-YsK0,37124
13
- tasktree-0.0.10.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
- tasktree-0.0.10.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
15
- tasktree-0.0.10.dist-info/RECORD,,
12
+ tasktree-0.0.12.dist-info/METADATA,sha256=fdh62_US58sghqn46-f0072dEnFECrJccRa6tL7m2DU,37234
13
+ tasktree-0.0.12.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ tasktree-0.0.12.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
15
+ tasktree-0.0.12.dist-info/RECORD,,