tasktree 0.0.4__py3-none-any.whl → 0.0.5__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
@@ -117,27 +117,28 @@ def _init_recipe():
117
117
  raise typer.Exit(1)
118
118
 
119
119
  template = """# Task Tree Recipe
120
- # See https://github.com/kevinchannon/task-tree for documentation
120
+ # See https://github.com/kevinchannon/tasktree for documentation
121
121
 
122
122
  # Example task definitions:
123
123
 
124
- # build:
125
- # desc: Compile the application
126
- # outputs: [target/release/bin]
127
- # cmd: cargo build --release
128
-
129
- # test:
130
- # desc: Run tests
131
- # deps: [build]
132
- # cmd: cargo test
133
-
134
- # deploy:
135
- # desc: Deploy to environment
136
- # deps: [build]
137
- # args: [environment, region=eu-west-1]
138
- # cmd: |
139
- # echo "Deploying to {{environment}} in {{region}}"
140
- # ./deploy.sh {{environment}} {{region}}
124
+ tasks:
125
+ # build:
126
+ # desc: Compile the application
127
+ # outputs: [target/release/bin]
128
+ # cmd: cargo build --release
129
+
130
+ # test:
131
+ # desc: Run tests
132
+ # deps: [build]
133
+ # cmd: cargo test
134
+
135
+ # deploy:
136
+ # desc: Deploy to environment
137
+ # deps: [build]
138
+ # args: [environment, region=eu-west-1]
139
+ # cmd: |
140
+ # echo "Deploying to {{environment}} in {{region}}"
141
+ # ./deploy.sh {{environment}} {{region}}
141
142
 
142
143
  # Uncomment and modify the examples above to define your tasks
143
144
  """
tasktree/parser.py CHANGED
@@ -270,7 +270,7 @@ def _parse_file(
270
270
  local_import_namespaces: set[str] = set()
271
271
 
272
272
  # Process nested imports FIRST
273
- imports = data.get("import", [])
273
+ imports = data.get("imports", [])
274
274
  if imports:
275
275
  for import_spec in imports:
276
276
  child_file = import_spec["file"]
@@ -297,15 +297,49 @@ def _parse_file(
297
297
 
298
298
  tasks.update(nested_tasks)
299
299
 
300
- # Determine where tasks are defined
301
- # Tasks can be either at root level OR inside a "tasks:" key
302
- tasks_data = data.get("tasks", data) if "tasks" in data else data
300
+ # Validate top-level keys (only imports, environments, and tasks are allowed)
301
+ VALID_TOP_LEVEL_KEYS = {"imports", "environments", "tasks"}
302
+
303
+ # Check if tasks key is missing when there appear to be task definitions at root
304
+ # Do this BEFORE checking for unknown keys, to provide better error message
305
+ if "tasks" not in data and data:
306
+ # Check if there are potential task definitions at root level
307
+ potential_tasks = [
308
+ k for k, v in data.items()
309
+ if isinstance(v, dict) and k not in VALID_TOP_LEVEL_KEYS
310
+ ]
311
+
312
+ if potential_tasks:
313
+ raise ValueError(
314
+ f"Invalid recipe format in {file_path}\n\n"
315
+ f"Task definitions must be under a top-level 'tasks:' key.\n\n"
316
+ f"Found these keys at root level: {', '.join(potential_tasks)}\n\n"
317
+ f"Did you mean:\n\n"
318
+ f"tasks:\n"
319
+ + '\n'.join(f" {k}:" for k in potential_tasks) +
320
+ "\n cmd: ...\n\n"
321
+ f"Valid top-level keys are: {', '.join(sorted(VALID_TOP_LEVEL_KEYS))}"
322
+ )
323
+
324
+ # Now check for other invalid top-level keys (non-dict values)
325
+ invalid_keys = set(data.keys()) - VALID_TOP_LEVEL_KEYS
326
+ if invalid_keys:
327
+ raise ValueError(
328
+ f"Invalid recipe format in {file_path}\n\n"
329
+ f"Unknown top-level keys: {', '.join(sorted(invalid_keys))}\n\n"
330
+ f"Valid top-level keys are:\n"
331
+ f" - imports (for importing task files)\n"
332
+ f" - environments (for shell environment configuration)\n"
333
+ f" - tasks (for task definitions)"
334
+ )
335
+
336
+ # Extract tasks from "tasks" key
337
+ tasks_data = data.get("tasks", {})
338
+ if tasks_data is None:
339
+ tasks_data = {}
303
340
 
304
341
  # Process local tasks
305
342
  for task_name, task_data in tasks_data.items():
306
- # Skip special sections (only relevant if tasks are at root level)
307
- if task_name in ("import", "environments", "tasks"):
308
- continue
309
343
 
310
344
  if not isinstance(task_data, dict):
311
345
  raise ValueError(f"Task '{task_name}' must be a dictionary")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -70,33 +70,34 @@ Is this just whining and moaning? Should we just man up and revel in our own abi
70
70
 
71
71
  ... No. That's **a dumb idea**.
72
72
 
73
- Task Tree allows you to pile all the knowledge of **what** to run, **when** to run it, **where** to run it and **how** to run it into a single, readable place. Then you can delete all the scripts that no-one knows how to use and all the readme docs that lie to the few people that actually waste their time reading them.
73
+ Task Tree allows you to pile all the knowledge of **what** to run, **when** to run it, **where** to run it and **how** to run it into a single, readable place. Then you can delete all the scripts that no-one knows how to use and all the readme docs that lie to the few people that actually waste their time reading them.
74
74
 
75
75
  The tasks you need to perform to deliver your project become summarised in an executable file that looks like:
76
76
  ```yaml
77
- build:
78
- desc: Compile stuff
79
- outputs: [target/release/bin]
80
- cmd: cargo build --release
81
-
82
- package:
83
- desc: build installers
84
- deps: [build]
85
- outputs: [awesome.deb]
86
- cmd: |
87
- for bin in target/release/*; do
88
- if [[ -x "$bin" && ! -d "$bin" ]]; then
89
- install -Dm755 "$bin" "debian/awesome/usr/bin/$(basename "$bin")"
90
- fi
91
- done
92
-
93
- dpkg-buildpackage -us -uc
94
-
95
- test:
96
- desc: Run tests
97
- deps: [package]
98
- inputs: [tests/**/*.py]
99
- cmd: PYTHONPATH=src python3 -m pytest tests/ -v
77
+ tasks:
78
+ build:
79
+ desc: Compile stuff
80
+ outputs: [target/release/bin]
81
+ cmd: cargo build --release
82
+
83
+ package:
84
+ desc: build installers
85
+ deps: [build]
86
+ outputs: [awesome.deb]
87
+ cmd: |
88
+ for bin in target/release/*; do
89
+ if [[ -x "$bin" && ! -d "$bin" ]]; then
90
+ install -Dm755 "$bin" "debian/awesome/usr/bin/$(basename "$bin")"
91
+ fi
92
+ done
93
+
94
+ dpkg-buildpackage -us -uc
95
+
96
+ test:
97
+ desc: Run tests
98
+ deps: [package]
99
+ inputs: [tests/**/*.py]
100
+ cmd: PYTHONPATH=src python3 -m pytest tests/ -v
100
101
  ```
101
102
 
102
103
  If you want to run the tests then:
@@ -107,6 +108,52 @@ Boom! Done. `build` will always run, because there's no sensible way to know wha
107
108
 
108
109
  This is a toy example, but you can image how it plays out on a more complex project.
109
110
 
111
+ ## Migrating from v1.x to v2.0
112
+
113
+ Version 2.0 requires all task definitions to be under a top-level `tasks:` key.
114
+
115
+ ### Quick Migration
116
+
117
+ Wrap your existing tasks in a `tasks:` block:
118
+
119
+ ```yaml
120
+ # Before (v1.x)
121
+ build:
122
+ cmd: cargo build
123
+
124
+ # After (v2.0)
125
+ tasks:
126
+ build:
127
+ cmd: cargo build
128
+ ```
129
+
130
+ ### Why This Change?
131
+
132
+ 1. **Clearer structure**: Explicit separation of tasks from configuration
133
+ 2. **No naming conflicts**: You can now create tasks named "imports" or "environments"
134
+ 3. **Better error messages**: More helpful validation errors
135
+ 4. **Consistency**: All recipe files use the same format
136
+
137
+ ### Error Messages
138
+
139
+ If you forget to update, you'll see a clear error:
140
+
141
+ ```
142
+ Invalid recipe format in tasktree.yaml
143
+
144
+ Task definitions must be under a top-level "tasks:" key.
145
+
146
+ Found these keys at root level: build, test
147
+
148
+ Did you mean:
149
+
150
+ tasks:
151
+ build:
152
+ cmd: ...
153
+ test:
154
+ cmd: ...
155
+ ```
156
+
110
157
  ## Installation
111
158
 
112
159
  ### From PyPI (Recommended)
@@ -146,15 +193,16 @@ pipx install .
146
193
  Create a `tasktree.yaml` (or `tt.yaml`) in your project:
147
194
 
148
195
  ```yaml
149
- build:
150
- desc: Compile the application
151
- outputs: [target/release/bin]
152
- cmd: cargo build --release
196
+ tasks:
197
+ build:
198
+ desc: Compile the application
199
+ outputs: [target/release/bin]
200
+ cmd: cargo build --release
153
201
 
154
- test:
155
- desc: Run tests
156
- deps: [build]
157
- cmd: cargo test
202
+ test:
203
+ desc: Run tests
204
+ deps: [build]
205
+ cmd: cargo test
158
206
  ```
159
207
 
160
208
  Run tasks:
@@ -185,15 +233,16 @@ Task Tree only runs tasks when necessary. A task executes if:
185
233
  Tasks automatically inherit inputs from dependencies, eliminating redundant declarations:
186
234
 
187
235
  ```yaml
188
- build:
189
- outputs: [dist/app]
190
- cmd: go build -o dist/app
191
-
192
- package:
193
- deps: [build]
194
- outputs: [dist/app.tar.gz]
195
- cmd: tar czf dist/app.tar.gz dist/app
196
- # Automatically tracks dist/app as an input
236
+ tasks:
237
+ build:
238
+ outputs: [dist/app]
239
+ cmd: go build -o dist/app
240
+
241
+ package:
242
+ deps: [build]
243
+ outputs: [dist/app.tar.gz]
244
+ cmd: tar czf dist/app.tar.gz dist/app
245
+ # Automatically tracks dist/app as an input
197
246
  ```
198
247
 
199
248
  ### Single State File
@@ -205,15 +254,16 @@ All state lives in `.tasktree-state` at your project root. Stale entries are aut
205
254
  ### Basic Structure
206
255
 
207
256
  ```yaml
208
- task-name:
209
- desc: Human-readable description (optional)
210
- deps: [other-task] # Task dependencies
211
- inputs: [src/**/*.go] # Explicit input files (glob patterns)
212
- outputs: [dist/binary] # Output files (glob patterns)
213
- working_dir: subproject/ # Execution directory (default: project root)
214
- env: bash-strict # Execution environment (optional)
215
- args: [param1, param2:path=default] # Task parameters
216
- cmd: go build -o dist/binary # Command to execute
257
+ tasks:
258
+ task-name:
259
+ desc: Human-readable description (optional)
260
+ deps: [other-task] # Task dependencies
261
+ inputs: [src/**/*.go] # Explicit input files (glob patterns)
262
+ outputs: [dist/binary] # Output files (glob patterns)
263
+ working_dir: subproject/ # Execution directory (default: project root)
264
+ env: bash-strict # Execution environment (optional)
265
+ args: [param1, param2:path=default] # Task parameters
266
+ cmd: go build -o dist/binary # Command to execute
217
267
  ```
218
268
 
219
269
  ### Commands
@@ -221,18 +271,20 @@ task-name:
221
271
  **Single-line commands** are executed directly via the configured shell:
222
272
 
223
273
  ```yaml
224
- build:
225
- cmd: cargo build --release
274
+ tasks:
275
+ build:
276
+ cmd: cargo build --release
226
277
  ```
227
278
 
228
279
  **Multi-line commands** are written to temporary script files for proper execution:
229
280
 
230
281
  ```yaml
231
- deploy:
232
- cmd: |
233
- mkdir -p dist
234
- cp build/* dist/
235
- rsync -av dist/ server:/opt/app/
282
+ tasks:
283
+ deploy:
284
+ cmd: |
285
+ mkdir -p dist
286
+ cp build/* dist/
287
+ rsync -av dist/ server:/opt/app/
236
288
  ```
237
289
 
238
290
  Multi-line commands preserve shell syntax (line continuations, heredocs, etc.) and support shebangs on Unix/macOS.
@@ -240,12 +292,13 @@ Multi-line commands preserve shell syntax (line continuations, heredocs, etc.) a
240
292
  Or use folded blocks for long single-line commands:
241
293
 
242
294
  ```yaml
243
- compile:
244
- cmd: >
245
- gcc -o bin/app
246
- src/*.c
247
- -I include
248
- -L lib -lm
295
+ tasks:
296
+ compile:
297
+ cmd: >
298
+ gcc -o bin/app
299
+ src/*.c
300
+ -I include
301
+ -L lib -lm
249
302
  ```
250
303
 
251
304
  ### Execution Environments
@@ -305,12 +358,13 @@ tasks:
305
358
  Tasks can accept arguments with optional defaults:
306
359
 
307
360
  ```yaml
308
- deploy:
309
- args: [environment, region=eu-west-1]
310
- deps: [build]
311
- cmd: |
312
- aws s3 cp dist/app.zip s3://{{environment}}-{{region}}/
313
- aws lambda update-function-code --function-name app-{{environment}}
361
+ tasks:
362
+ deploy:
363
+ args: [environment, region=eu-west-1]
364
+ deps: [build]
365
+ cmd: |
366
+ aws s3 cp dist/app.zip s3://{{environment}}-{{region}}/
367
+ aws lambda update-function-code --function-name app-{{environment}}
314
368
  ```
315
369
 
316
370
  Invoke with: `tt deploy production` or `tt deploy staging us-east-1` or `tt deploy staging region=us-east-1`.
@@ -337,18 +391,19 @@ Split task definitions across multiple files for better organisation:
337
391
 
338
392
  ```yaml
339
393
  # tasktree.yaml
340
- import:
394
+ imports:
341
395
  - file: build/tasks.yml
342
396
  as: build
343
397
  - file: deploy/tasks.yml
344
398
  as: deploy
345
399
 
346
- test:
347
- deps: [build.compile, build.test-compile]
348
- cmd: ./run-tests.sh
400
+ tasks:
401
+ test:
402
+ deps: [build.compile, build.test-compile]
403
+ cmd: ./run-tests.sh
349
404
 
350
- ci:
351
- deps: [build.all, test, deploy.staging]
405
+ ci:
406
+ deps: [build.all, test, deploy.staging]
352
407
  ```
353
408
 
354
409
  Imported tasks are namespaced and can be referenced as dependencies. Each imported file is self-contained—it cannot depend on tasks in the importing file.
@@ -466,41 +521,42 @@ imports:
466
521
  - file: common/docker.yml
467
522
  as: docker
468
523
 
469
- compile:
470
- desc: Build application binaries
471
- outputs: [target/release/app]
472
- cmd: cargo build --release
473
-
474
- test-unit:
475
- desc: Run unit tests
476
- deps: [compile]
477
- cmd: cargo test
478
-
479
- package:
480
- desc: Create distribution archive
481
- deps: [compile]
482
- outputs: [dist/app-{{version}}.tar.gz]
483
- args: [version]
484
- cmd: |
485
- mkdir -p dist
486
- tar czf dist/app-{{version}}.tar.gz \
487
- target/release/app \
488
- config/ \
489
- migrations/
490
-
491
- deploy:
492
- desc: Deploy to environment
493
- deps: [package, docker.build-runtime]
494
- args: [environment, version]
495
- cmd: |
496
- scp dist/app-{{version}}.tar.gz {{environment}}:/opt/
497
- ssh {{environment}} /opt/deploy.sh {{version}}
498
-
499
- integration-test:
500
- desc: Run integration tests against deployed environment
501
- deps: [deploy]
502
- args: [environment, version]
503
- cmd: pytest tests/integration/ --env={{environment}}
524
+ tasks:
525
+ compile:
526
+ desc: Build application binaries
527
+ outputs: [target/release/app]
528
+ cmd: cargo build --release
529
+
530
+ test-unit:
531
+ desc: Run unit tests
532
+ deps: [compile]
533
+ cmd: cargo test
534
+
535
+ package:
536
+ desc: Create distribution archive
537
+ deps: [compile]
538
+ outputs: [dist/app-{{version}}.tar.gz]
539
+ args: [version]
540
+ cmd: |
541
+ mkdir -p dist
542
+ tar czf dist/app-{{version}}.tar.gz \
543
+ target/release/app \
544
+ config/ \
545
+ migrations/
546
+
547
+ deploy:
548
+ desc: Deploy to environment
549
+ deps: [package, docker.build-runtime]
550
+ args: [environment, version]
551
+ cmd: |
552
+ scp dist/app-{{version}}.tar.gz {{environment}}:/opt/
553
+ ssh {{environment}} /opt/deploy.sh {{version}}
554
+
555
+ integration-test:
556
+ desc: Run integration tests against deployed environment
557
+ deps: [deploy]
558
+ args: [environment, version]
559
+ cmd: pytest tests/integration/ --env={{environment}}
504
560
  ```
505
561
 
506
562
  Run the full pipeline:
@@ -1,13 +1,13 @@
1
1
  tasktree/__init__.py,sha256=MVmdvKb3JdqLlo0x2_TPGMfgFC0HsDnP79HAzGnFnjI,1081
2
- tasktree/cli.py,sha256=2Pm0pxWdG4RaQbVMijaMUIrm3v0b9J98CUsUp7cDFvI,13236
2
+ tasktree/cli.py,sha256=jeLFmyFjTeF6JXDPLI46H5Xao9br68tPWL7D6LbC7u0,13272
3
3
  tasktree/executor.py,sha256=_E37tShHuiOj0Mvx2GbS9y3GIozC3hpzAVhAjbvYJqg,18638
4
4
  tasktree/graph.py,sha256=9ngfg93y7EkOIN_lUQa0u-JhnwiMN1UdQQvIFw8RYCE,4181
5
5
  tasktree/hasher.py,sha256=puJey9wF_p37k_xqjhYr_6ICsbAfrTBWHec6MqKV4BU,814
6
- tasktree/parser.py,sha256=apLfN3_YVa7lQIy0rcHFwo931Pg-8mKG1044AQE6fLQ,12690
6
+ tasktree/parser.py,sha256=5WXoqN2cUtO9icIPDSvK5889XinBcAQMBCyNoJ2ZO6w,14152
7
7
  tasktree/state.py,sha256=rxKtS3SbsPtAuraHbN807RGWfoYYkQ3pe8CxUstwo2k,3535
8
8
  tasktree/tasks.py,sha256=2QdQZtJAX2rSGbyXKG1z9VF_siz1DUzdvzCgPkykxtU,173
9
9
  tasktree/types.py,sha256=w--sKjRTc8mGYkU5eAduqV86SolDqOYspAPuVKIuSQQ,3797
10
- tasktree-0.0.4.dist-info/METADATA,sha256=dxJ5QiaRWhBbae-cK358ESQuwbjDg04ITWomlgrO9Lc,16312
11
- tasktree-0.0.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- tasktree-0.0.4.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
13
- tasktree-0.0.4.dist-info/RECORD,,
10
+ tasktree-0.0.5.dist-info/METADATA,sha256=ncZRu7zCKZRd5YGG5fAWAL1TtH1mXbtnTqAne3RHdeA,17456
11
+ tasktree-0.0.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ tasktree-0.0.5.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
13
+ tasktree-0.0.5.dist-info/RECORD,,