vortex-cli 8.1.1__tar.gz → 8.2.0__tar.gz

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.
Files changed (58) hide show
  1. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/PKG-INFO +75 -9
  2. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/README.md +74 -8
  3. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/pyproject.toml +1 -1
  4. vortex_cli-8.2.0/vortex/commands/agenda.py +403 -0
  5. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/execute.py +0 -6
  6. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/object_.py +58 -26
  7. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/main.py +10 -1
  8. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/registry.py +4 -0
  9. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/webdesign.py +20 -5
  10. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex_cli.egg-info/PKG-INFO +75 -9
  11. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex_cli.egg-info/SOURCES.txt +1 -0
  12. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/LICENSE +0 -0
  13. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/setup.cfg +0 -0
  14. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/__init__.py +0 -0
  15. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/__main__.py +0 -0
  16. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/cli.py +0 -0
  17. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/colour.py +0 -0
  18. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/__init__.py +0 -0
  19. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/app.py +0 -0
  20. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/clean.py +0 -0
  21. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/clone.py +0 -0
  22. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/code.py +0 -0
  23. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/compile.py +0 -0
  24. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/db.py +0 -0
  25. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/diff.py +0 -0
  26. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/docs.py +0 -0
  27. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/fetch.py +0 -0
  28. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/info.py +0 -0
  29. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/keyword.py +0 -0
  30. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/libs.py +0 -0
  31. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/log.py +0 -0
  32. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/pull.py +0 -0
  33. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/search.py +0 -0
  34. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/servers.py +0 -0
  35. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/use.py +0 -0
  36. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/commands/watch.py +0 -0
  37. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/constants.py +0 -0
  38. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/docs/Blackbook v2.md +0 -0
  39. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/docs/Blackbook.pdf +0 -0
  40. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/docs/index.html +0 -0
  41. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/docs/marked.min.js +0 -0
  42. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/git.py +0 -0
  43. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/lib/puakma-6.0.40.jar +0 -0
  44. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/libs.py +0 -0
  45. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/logging.py +0 -0
  46. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/models.py +0 -0
  47. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/output.py +0 -0
  48. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/schedule.py +0 -0
  49. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/server_options.py +0 -0
  50. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/soap.py +0 -0
  51. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/spinner.py +0 -0
  52. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/sync.py +0 -0
  53. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/util.py +0 -0
  54. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex/workspace.py +0 -0
  55. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex_cli.egg-info/dependency_links.txt +0 -0
  56. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex_cli.egg-info/entry_points.txt +0 -0
  57. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex_cli.egg-info/requires.txt +0 -0
  58. {vortex_cli-8.1.1 → vortex_cli-8.2.0}/vortex_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex_cli
3
- Version: 8.1.1
3
+ Version: 8.2.0
4
4
  Summary: Vortex CLI
5
5
  Author-email: Jordan Amos <jordan.amos@gmail.com>
6
6
  License: MIT License
@@ -106,6 +106,18 @@ While it is possible to use without it, this software has been purposefully desi
106
106
  java_environment_name = JavaSE-17 ; Java Execution Environment name https://docs.osgi.org/reference/eenames.html
107
107
  ```
108
108
 
109
+ ## Upgrading to 8.2
110
+
111
+ - **`object run` runs ACTIONs, and waits.** It calls the gateway's new `POST run` (gateway
112
+ `AppVersion` 0.1.4 or later) instead of sending `tell agenda run` to the console, and
113
+ reports how long the action took, its reply and any exception (see
114
+ [Running an action](#running-an-action)). An older gateway refuses it with `NOT_EXPOSED`.
115
+ It no longer accepts a SCHEDULED_ACTION.
116
+ - **`vortex agenda`** lists the scheduled actions AGENDA has loaded and queues one with
117
+ `agenda run`, refusing the cases where AGENDA would silently do nothing (see
118
+ [The agenda](#the-agenda)). It replaces both `object run` on a scheduled action and
119
+ `vortex execute tell agenda run ...`.
120
+
109
121
  ## Upgrading to 8.1
110
122
 
111
123
  8.1 does two things. It replaces `vortex config` with plain top-level commands that read
@@ -289,7 +301,7 @@ vortex object list [QUERY] [--app-id ID [--local]] [--strict --inherits-from|-
289
301
  --ids-only --show-params --type T...] [--json]
290
302
  vortex object grep PATTERN [--app-id ID] [--output-paths|--output-apps]
291
303
  [--include-resources|--type T...] [--json]
292
- vortex object run ID --app-id ID
304
+ vortex object run ID --app-id ID [--output] [--json]
293
305
  vortex object delete ID... --app-id ID [--yes]
294
306
 
295
307
  ── keyword ──────────────────────────────────────────────────────────────────
@@ -315,6 +327,10 @@ vortex db update-column DB TABLE COLUMN --app-id ID [--name --type --size --desc
315
327
  --cascade-delete|--no-cascade-delete]
316
328
  vortex db delete-column DB TABLE COLUMN --app-id ID [--yes]
317
329
 
330
+ ── agenda ───────────────────────────────────────────────────────────────────
331
+ vortex agenda [list] [--app-id ID] [--json]
332
+ vortex agenda run ID --app-id ID [--param KEY=VALUE ...] [--refresh --force] [--json]
333
+
318
334
  ── Workspace ────────────────────────────────────────────────────────────────
319
335
  vortex ls [app list filters] [--json] = app list --local
320
336
  vortex clone APP... [--group --reclone --all --get-resources --open-urls --timeout]
@@ -412,7 +428,7 @@ addins match:
412
428
 
413
429
  | Shortcut | Sends | Effect |
414
430
  |---|---|---|
415
- | `schedule` | `tell agenda schedule` | lists every scheduled action and its next run |
431
+ | `schedule` | `tell agenda schedule` | lists every scheduled action and its next run (raw text; `vortex agenda` parses it) |
416
432
  | `refresh-agenda` | `tell agenda refresh` | AGENDA rereads every schedule now |
417
433
  | `refresh-design APP_ID` | `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N` | rebuilds the application's design from its template, as `app refresh` does (a clone is then out of date) |
418
434
  | `flush-cache` | `tell http cache flush` | clears the design cache and all action class loaders |
@@ -420,12 +436,28 @@ addins match:
420
436
  `tell http flush cache` is **not** a valid command: the HTTP addin only matches
421
437
  `cache flush`, and anything else does nothing, silently. Use `flush-cache`.
422
438
 
423
- Two console operations act on one application or object, so they also live with those nouns:
424
-
425
- | Command | Sends |
426
- |---|---|
427
- | `vortex app refresh APP_ID` | `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N` - rebuilds the design from its template (a clone is then out of date) |
428
- | `vortex object run ID --app-id N` | `tell agenda run /group/app.pma/name` - runs an ACTION or SCHEDULED_ACTION now |
439
+ `vortex app refresh APP_ID` also lives with its noun: it sends
440
+ `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N`, which rebuilds the design from its
441
+ template (a clone is then out of date).
442
+
443
+ ### Running an action
444
+
445
+ `vortex object run ID --app-id N` runs an ACTION now, as your identity, and waits for it:
446
+ the gateway's `POST run` (`GatewaySystem`) loads the action's class, executes it and replies
447
+ with how long it took, its redirect, content type and reply size. `--output` also prints the
448
+ action's reply (its buffer, as text). An action that throws exits 1 with `ACTION_FAILED` and
449
+ the exception; a path with no compiled ACTION is `NOT_FOUND`.
450
+
451
+ - **ACTIONs only.** A SCHEDULED_ACTION (or any other type) is `WRONG_TYPE`. Scheduled
452
+ actions are queued with AGENDA: `vortex agenda run ID --app-id N` ([The agenda](#the-agenda)).
453
+ - **No client connection.** The action's `getOutputStream()` is null, as under AGENDA, so it
454
+ must buffer its reply (`write`/`setBuffer`); one that streams unconditionally fails with
455
+ the NullPointerException as its result.
456
+ - **Synchronous.** The CLI waits up to 300 s. Behind Cloudflare (prod) the request is cut
457
+ at about 100 s; the action still finishes on the server - check `vortex log`.
458
+ - **Gateway required.** It needs a gateway with the `run` route; an older gateway refuses with
459
+ `NOT_EXPOSED`. Earlier versions of `object run` sent `tell agenda run`, which only reached
460
+ scheduled actions on AGENDA's list and did nothing, silently, for an ACTION.
429
461
 
430
462
  The 8.0 flags (`--show-schedule`, `--refresh-agenda`, `--flush-cache`, `--refresh-design`,
431
463
  `--run`) were removed in 8.1. `app refresh` was `app refresh-design` in 8.1.0; the old
@@ -573,6 +605,40 @@ vortex object get 7901 --app-id 62 # the parsed schedule
573
605
  starts; if that lands between the command's read and write, the old `LastRun` is written
574
606
  back and the action may run one interval early.
575
607
 
608
+ ### The agenda
609
+
610
+ AGENDA has no API beyond its console commands, so `vortex agenda` sends them through the same
611
+ console route as `vortex execute` (the gateway's `POST console`, `GatewaySystem`, or the SOAP
612
+ console without the gateway) and parses the text they answer.
613
+
614
+ ```
615
+ vortex agenda -s dev # = agenda list
616
+ vortex agenda list --app-id 9 -s dev
617
+ vortex agenda run 7825 --app-id 60 -s dev [--param AppID=12] [--refresh] [--force]
618
+ ```
619
+
620
+ - **`list`** - `tell agenda schedule`: every scheduled action AGENDA has **loaded**, by next
621
+ run (server time), with the waitlist and AGENDA's counts (running, waitlisted, scheduled,
622
+ max at once). An action with empty `Options` is loaded as *on demand only*. Not listed: an
623
+ action with `Schedule=N`, one in a disabled application or an application with scheduled
624
+ actions disabled, and one created or renamed since AGENDA's last refresh (every 15
625
+ minutes). AGENDA says how many actions are running, never which.
626
+ - **`run`** - queues a SCHEDULED_ACTION: `tell agenda run /group/app.pma/Name[?&KEY=VALUE...]`.
627
+ It runs as the system session when one of AGENDA's slots is free; its result is only in
628
+ `vortex log`. AGENDA answers that command with nothing, whatever it did, so `run` checks
629
+ first and sends nothing when AGENDA would not run it:
630
+ - `NOT_ON_AGENDA` - the action has `Schedule=N` (give it a schedule first), or it is not on
631
+ the loaded list. `--refresh` sends `tell agenda refresh` and checks again (for an action
632
+ created or renamed in the last 15 minutes).
633
+ - `ALREADY_QUEUED` - it is already waitlisted; AGENDA would skip it.
634
+ - `PREFIX_COLLISION` - AGENDA matches the path by prefix and queues it once per matching
635
+ item, so `.../SyncAll` would also match an item `.../Sync` and run twice. `--force` sends
636
+ it anyway.
637
+ - `WRONG_TYPE` - not a SCHEDULED_ACTION (an ACTION is run with `vortex object run`).
638
+ - A queued run moves the action's next scheduled run (AGENDA resets it), and it can't be
639
+ stopped from here (`vortex execute tell agenda stop /group/app.pma/Name` asks the action to
640
+ quit; it has to check for that itself).
641
+
576
642
  ### Keyword values and secrets
577
643
 
578
644
  Keywords are an application's live configuration, and in practice they hold credentials in
@@ -62,6 +62,18 @@ While it is possible to use without it, this software has been purposefully desi
62
62
  java_environment_name = JavaSE-17 ; Java Execution Environment name https://docs.osgi.org/reference/eenames.html
63
63
  ```
64
64
 
65
+ ## Upgrading to 8.2
66
+
67
+ - **`object run` runs ACTIONs, and waits.** It calls the gateway's new `POST run` (gateway
68
+ `AppVersion` 0.1.4 or later) instead of sending `tell agenda run` to the console, and
69
+ reports how long the action took, its reply and any exception (see
70
+ [Running an action](#running-an-action)). An older gateway refuses it with `NOT_EXPOSED`.
71
+ It no longer accepts a SCHEDULED_ACTION.
72
+ - **`vortex agenda`** lists the scheduled actions AGENDA has loaded and queues one with
73
+ `agenda run`, refusing the cases where AGENDA would silently do nothing (see
74
+ [The agenda](#the-agenda)). It replaces both `object run` on a scheduled action and
75
+ `vortex execute tell agenda run ...`.
76
+
65
77
  ## Upgrading to 8.1
66
78
 
67
79
  8.1 does two things. It replaces `vortex config` with plain top-level commands that read
@@ -245,7 +257,7 @@ vortex object list [QUERY] [--app-id ID [--local]] [--strict --inherits-from|-
245
257
  --ids-only --show-params --type T...] [--json]
246
258
  vortex object grep PATTERN [--app-id ID] [--output-paths|--output-apps]
247
259
  [--include-resources|--type T...] [--json]
248
- vortex object run ID --app-id ID
260
+ vortex object run ID --app-id ID [--output] [--json]
249
261
  vortex object delete ID... --app-id ID [--yes]
250
262
 
251
263
  ── keyword ──────────────────────────────────────────────────────────────────
@@ -271,6 +283,10 @@ vortex db update-column DB TABLE COLUMN --app-id ID [--name --type --size --desc
271
283
  --cascade-delete|--no-cascade-delete]
272
284
  vortex db delete-column DB TABLE COLUMN --app-id ID [--yes]
273
285
 
286
+ ── agenda ───────────────────────────────────────────────────────────────────
287
+ vortex agenda [list] [--app-id ID] [--json]
288
+ vortex agenda run ID --app-id ID [--param KEY=VALUE ...] [--refresh --force] [--json]
289
+
274
290
  ── Workspace ────────────────────────────────────────────────────────────────
275
291
  vortex ls [app list filters] [--json] = app list --local
276
292
  vortex clone APP... [--group --reclone --all --get-resources --open-urls --timeout]
@@ -368,7 +384,7 @@ addins match:
368
384
 
369
385
  | Shortcut | Sends | Effect |
370
386
  |---|---|---|
371
- | `schedule` | `tell agenda schedule` | lists every scheduled action and its next run |
387
+ | `schedule` | `tell agenda schedule` | lists every scheduled action and its next run (raw text; `vortex agenda` parses it) |
372
388
  | `refresh-agenda` | `tell agenda refresh` | AGENDA rereads every schedule now |
373
389
  | `refresh-design APP_ID` | `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N` | rebuilds the application's design from its template, as `app refresh` does (a clone is then out of date) |
374
390
  | `flush-cache` | `tell http cache flush` | clears the design cache and all action class loaders |
@@ -376,12 +392,28 @@ addins match:
376
392
  `tell http flush cache` is **not** a valid command: the HTTP addin only matches
377
393
  `cache flush`, and anything else does nothing, silently. Use `flush-cache`.
378
394
 
379
- Two console operations act on one application or object, so they also live with those nouns:
380
-
381
- | Command | Sends |
382
- |---|---|
383
- | `vortex app refresh APP_ID` | `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N` - rebuilds the design from its template (a clone is then out of date) |
384
- | `vortex object run ID --app-id N` | `tell agenda run /group/app.pma/name` - runs an ACTION or SCHEDULED_ACTION now |
395
+ `vortex app refresh APP_ID` also lives with its noun: it sends
396
+ `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N`, which rebuilds the design from its
397
+ template (a clone is then out of date).
398
+
399
+ ### Running an action
400
+
401
+ `vortex object run ID --app-id N` runs an ACTION now, as your identity, and waits for it:
402
+ the gateway's `POST run` (`GatewaySystem`) loads the action's class, executes it and replies
403
+ with how long it took, its redirect, content type and reply size. `--output` also prints the
404
+ action's reply (its buffer, as text). An action that throws exits 1 with `ACTION_FAILED` and
405
+ the exception; a path with no compiled ACTION is `NOT_FOUND`.
406
+
407
+ - **ACTIONs only.** A SCHEDULED_ACTION (or any other type) is `WRONG_TYPE`. Scheduled
408
+ actions are queued with AGENDA: `vortex agenda run ID --app-id N` ([The agenda](#the-agenda)).
409
+ - **No client connection.** The action's `getOutputStream()` is null, as under AGENDA, so it
410
+ must buffer its reply (`write`/`setBuffer`); one that streams unconditionally fails with
411
+ the NullPointerException as its result.
412
+ - **Synchronous.** The CLI waits up to 300 s. Behind Cloudflare (prod) the request is cut
413
+ at about 100 s; the action still finishes on the server - check `vortex log`.
414
+ - **Gateway required.** It needs a gateway with the `run` route; an older gateway refuses with
415
+ `NOT_EXPOSED`. Earlier versions of `object run` sent `tell agenda run`, which only reached
416
+ scheduled actions on AGENDA's list and did nothing, silently, for an ACTION.
385
417
 
386
418
  The 8.0 flags (`--show-schedule`, `--refresh-agenda`, `--flush-cache`, `--refresh-design`,
387
419
  `--run`) were removed in 8.1. `app refresh` was `app refresh-design` in 8.1.0; the old
@@ -529,6 +561,40 @@ vortex object get 7901 --app-id 62 # the parsed schedule
529
561
  starts; if that lands between the command's read and write, the old `LastRun` is written
530
562
  back and the action may run one interval early.
531
563
 
564
+ ### The agenda
565
+
566
+ AGENDA has no API beyond its console commands, so `vortex agenda` sends them through the same
567
+ console route as `vortex execute` (the gateway's `POST console`, `GatewaySystem`, or the SOAP
568
+ console without the gateway) and parses the text they answer.
569
+
570
+ ```
571
+ vortex agenda -s dev # = agenda list
572
+ vortex agenda list --app-id 9 -s dev
573
+ vortex agenda run 7825 --app-id 60 -s dev [--param AppID=12] [--refresh] [--force]
574
+ ```
575
+
576
+ - **`list`** - `tell agenda schedule`: every scheduled action AGENDA has **loaded**, by next
577
+ run (server time), with the waitlist and AGENDA's counts (running, waitlisted, scheduled,
578
+ max at once). An action with empty `Options` is loaded as *on demand only*. Not listed: an
579
+ action with `Schedule=N`, one in a disabled application or an application with scheduled
580
+ actions disabled, and one created or renamed since AGENDA's last refresh (every 15
581
+ minutes). AGENDA says how many actions are running, never which.
582
+ - **`run`** - queues a SCHEDULED_ACTION: `tell agenda run /group/app.pma/Name[?&KEY=VALUE...]`.
583
+ It runs as the system session when one of AGENDA's slots is free; its result is only in
584
+ `vortex log`. AGENDA answers that command with nothing, whatever it did, so `run` checks
585
+ first and sends nothing when AGENDA would not run it:
586
+ - `NOT_ON_AGENDA` - the action has `Schedule=N` (give it a schedule first), or it is not on
587
+ the loaded list. `--refresh` sends `tell agenda refresh` and checks again (for an action
588
+ created or renamed in the last 15 minutes).
589
+ - `ALREADY_QUEUED` - it is already waitlisted; AGENDA would skip it.
590
+ - `PREFIX_COLLISION` - AGENDA matches the path by prefix and queues it once per matching
591
+ item, so `.../SyncAll` would also match an item `.../Sync` and run twice. `--force` sends
592
+ it anyway.
593
+ - `WRONG_TYPE` - not a SCHEDULED_ACTION (an ACTION is run with `vortex object run`).
594
+ - A queued run moves the action's next scheduled run (AGENDA resets it), and it can't be
595
+ stopped from here (`vortex execute tell agenda stop /group/app.pma/Name` asks the action to
596
+ quit; it has to check for that itself).
597
+
532
598
  ### Keyword values and secrets
533
599
 
534
600
  Keywords are an application's live configuration, and in practice they hold credentials in
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "vortex_cli"
8
- version = "8.1.1"
8
+ version = "8.2.0"
9
9
  description = "Vortex CLI"
10
10
  requires-python = ">=3.10"
11
11
  readme = { file = "README.md", content-type = "text/markdown" }
@@ -0,0 +1,403 @@
1
+ """
2
+ vortex agenda [list] | run - the server's scheduled actions, through AGENDA.
3
+
4
+ AGENDA (puakma.addin.agenda) has no API beyond its console commands, so
5
+ both verbs go through the console route 'vortex execute' uses (the
6
+ gateway's POST console, GatewaySystem, or the SOAP console without the
7
+ gateway) and parse AGENDA's text replies:
8
+
9
+ tell agenda schedule one line per LOADED item, sorted by path:
10
+ '/grp/app.pma/Name 1D NextRun=Fri 09.Oct.26 22:00:00'
11
+ tell agenda status counts: running, waitlisted, scheduled, ...
12
+ tell agenda waitlist status one line per queued item: '/grp/app.pma/Name Running=...'
13
+
14
+ 'list' shows only what AGENDA has loaded: an action in a disabled app,
15
+ an app with scheduled actions disabled, or one created since the last
16
+ refresh (every 15 minutes) is not on it. No console command says which
17
+ actions are running - only how many.
18
+
19
+ 'run' sends 'tell agenda run /grp/app.pma/Name', which AGENDA matches
20
+ against its loaded list and queues (AGENDA.forceRun). That command
21
+ answers nothing either way, so 'run' checks first what AGENDA would do:
22
+
23
+ - Not on the list: AGENDA ignores the command silently - NOT_ON_AGENDA
24
+ (--refresh rereads the list first). 'Schedule=N' in the Options is
25
+ never loaded; EMPTY Options load as an on-demand item.
26
+ - Already waitlisted: AGENDA skips it - ALREADY_QUEUED.
27
+ - AGENDA matches by PREFIX (sent path startsWith item path, case-
28
+ insensitive) and queues the sent path once per matching item, so
29
+ 'SyncAll' also matches an item 'Sync' and runs twice - PREFIX_COLLISION
30
+ unless --force.
31
+
32
+ A queued run executes as the system session when a slot is free; its
33
+ result is only in the server log. ACTIONs are run with 'object run'.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import argparse
39
+ import datetime
40
+ import logging
41
+ import re
42
+ from typing import Any
43
+
44
+ from vortex import cli
45
+ from vortex import registry
46
+ from vortex.commands import execute
47
+ from vortex.models import DesignType
48
+ from vortex.output import CommandError
49
+ from vortex.output import render_table
50
+ from vortex.output import text
51
+ from vortex.registry import Command
52
+ from vortex.registry import Context
53
+ from vortex.registry import usage
54
+ from vortex.webdesign import to_int
55
+
56
+ logger = logging.getLogger("vortex")
57
+
58
+ SCHEDULE_CMD = execute.AGENDA_SCHEDULE_CMD
59
+ REFRESH_CMD = execute.AGENDA_REFRESH_CMD
60
+ STATUS_CMD = "tell agenda status"
61
+ WAITLIST_CMD = "tell agenda waitlist status"
62
+ RUN_CMD = "tell agenda run %s"
63
+
64
+ # AgendaItem.getSchedule(): path, interval + type letter, next run
65
+ _SCHEDULE_LINE = re.compile(r"^(/\S+)\s+(\d+)([A-Z])\s+NextRun=(.+?)\s*$")
66
+ # AgendaAction.toString(): path, Running=, LastRun=
67
+ _WAITLIST_LINE = re.compile(r"^(/\S+)\s+Running=")
68
+ _STATUS_LINE = re.compile(r"^([A-Za-z][A-Za-z ]+):\s*(\d+)\s*$")
69
+ # AgendaItem's SimpleDateFormat for NextRun
70
+ _NEXT_RUN_FORMAT = "%a %d.%b.%y %H:%M:%S"
71
+ # AgendaItem.SCHED_* letters
72
+ _UNITS = {
73
+ "S": "sec",
74
+ "I": "min",
75
+ "H": "hour",
76
+ "D": "day",
77
+ "W": "week",
78
+ "M": "month",
79
+ "Y": "year",
80
+ }
81
+ _PARAM = re.compile(r"^[A-Za-z0-9_.-]+=[A-Za-z0-9_.:-]*$")
82
+
83
+
84
+ def _console(ctx: Context, command: str) -> str:
85
+ return execute.console_command(ctx.server, command) or ""
86
+
87
+
88
+ def parse_schedule(reply: str) -> list[dict[str, Any]]:
89
+ """The items of a 'tell agenda schedule' reply (lines that don't parse are skipped)."""
90
+ items = []
91
+ for line in reply.splitlines():
92
+ m = _SCHEDULE_LINE.match(line.strip())
93
+ if m is None:
94
+ continue
95
+ path, interval, unit, next_run = m.groups()
96
+ app_path, _, name = path.rpartition("/")
97
+ app = app_path.lstrip("/").removesuffix(".pma")
98
+ items.append(
99
+ {
100
+ "path": path,
101
+ "app": app,
102
+ "name": name,
103
+ "interval": int(interval),
104
+ "unit": unit,
105
+ "every": _every(int(interval), unit),
106
+ "nextRun": _next_run(next_run),
107
+ }
108
+ )
109
+ return items
110
+
111
+
112
+ def _every(interval: int, unit: str) -> str:
113
+ if unit == "N":
114
+ return "on demand"
115
+ word = _UNITS.get(unit)
116
+ if word is None:
117
+ return f"{interval}{unit}"
118
+ return f"{interval} {word}{'' if interval == 1 else 's'}"
119
+
120
+
121
+ def _next_run(value: str) -> str | None:
122
+ """NextRun as ISO 8601 (server time), None when UnScheduled, else as sent."""
123
+ if value == "UnScheduled":
124
+ return None
125
+ try:
126
+ return datetime.datetime.strptime(value, _NEXT_RUN_FORMAT).isoformat()
127
+ except ValueError:
128
+ return value
129
+
130
+
131
+ def parse_status(reply: str) -> dict[str, int]:
132
+ """The counts in a 'tell agenda status' reply, camelCase keys ('Actions running' -> running)."""
133
+ counts = {}
134
+ for line in reply.splitlines():
135
+ m = _STATUS_LINE.match(line.strip())
136
+ if m is None:
137
+ continue
138
+ words = m.group(1).lower().removeprefix("actions ").split()
139
+ key = words[0] + "".join(w.title() for w in words[1:])
140
+ counts[key] = int(m.group(2))
141
+ return counts
142
+
143
+
144
+ def parse_waitlist(reply: str) -> list[str]:
145
+ """The paths in a 'tell agenda waitlist status' reply."""
146
+ paths = []
147
+ for line in reply.splitlines():
148
+ m = _WAITLIST_LINE.match(line.strip())
149
+ if m is not None:
150
+ paths.append(m.group(1))
151
+ return paths
152
+
153
+
154
+ def _sort_key(item: dict[str, Any]) -> tuple[int, str, str]:
155
+ # scheduled first by next run, then on-demand items by path
156
+ next_run = item["nextRun"]
157
+ return (0, next_run, item["path"]) if next_run else (1, "", item["path"])
158
+
159
+
160
+ # -- list ------------------------------------------------------------------
161
+
162
+
163
+ def _list(ctx: Context, args: argparse.Namespace) -> dict[str, Any]:
164
+ items = parse_schedule(_console(ctx, SCHEDULE_CMD))
165
+ queued = {p.lower() for p in parse_waitlist(_console(ctx, WAITLIST_CMD))}
166
+ if args.app_id is not None:
167
+ from vortex.commands.app import find_app_row
168
+
169
+ app = find_app_row(ctx, args.app_id)
170
+ group = text(app.get("appgroup"))
171
+ want = f"{group + '/' if group else ''}{text(app.get('appname'))}".lower()
172
+ items = [i for i in items if i["app"].lower() == want]
173
+ for item in items:
174
+ item["waitlisted"] = item["path"].lower() in queued
175
+ return {
176
+ "status": parse_status(_console(ctx, STATUS_CMD)),
177
+ "items": sorted(items, key=_sort_key),
178
+ }
179
+
180
+
181
+ def _render_list(ctx: Context, args: argparse.Namespace, data: Any) -> None:
182
+ rows = [
183
+ (
184
+ i["app"],
185
+ i["name"],
186
+ i["every"],
187
+ (i["nextRun"] or "on demand only").replace("T", " "),
188
+ "waitlisted" if i["waitlisted"] else "",
189
+ )
190
+ for i in data["items"]
191
+ ]
192
+ if rows:
193
+ render_table(
194
+ rows, ["App", "Action", "Every", "Next run (server time)", "Queue"]
195
+ )
196
+ else:
197
+ print("No scheduled actions on the agenda")
198
+ status = data["status"]
199
+ if status:
200
+ print(
201
+ f"\n{status.get('scheduled', len(rows))} scheduled · "
202
+ f"{status.get('running', 0)} running · "
203
+ f"{status.get('waitlisted', 0)} waitlisted · "
204
+ f"max {status.get('maxConcurrentActions', '?')} at once"
205
+ )
206
+
207
+
208
+ def _configure_list(parser: argparse.ArgumentParser) -> None:
209
+ parser.add_argument(
210
+ "--app-id",
211
+ metavar="ID",
212
+ type=cli.positive_id,
213
+ help="Only this application's scheduled actions",
214
+ )
215
+
216
+
217
+ # -- run -------------------------------------------------------------------
218
+
219
+
220
+ def _params(args: argparse.Namespace) -> str:
221
+ """'?&k=v&k2=v2' for --param, as 'app refresh' sends RefreshDesign?&AppID=N."""
222
+ for param in args.param:
223
+ if not _PARAM.match(param):
224
+ usage(
225
+ args,
226
+ f"--param '{param}' must be KEY=VALUE (letters, digits, '_', '.', "
227
+ "'-'; the value may also hold ':')",
228
+ )
229
+ return "?&" + "&".join(args.param) if args.param else ""
230
+
231
+
232
+ def _run(ctx: Context, args: argparse.Namespace) -> dict[str, Any]:
233
+ from vortex.commands.app import find_app_row
234
+
235
+ query = _params(args)
236
+ app = find_app_row(ctx, args.app_id)
237
+ row = ctx.client.get_design(args.app_id, args.id)
238
+ name = text(row.get("name"))
239
+ design_type = to_int(row.get("designtype") or 0)
240
+ if design_type != DesignType.SCHEDULED_ACTION:
241
+ try:
242
+ type_name = DesignType(design_type).name
243
+ except ValueError:
244
+ type_name = text(row.get("designtype"))
245
+ raise CommandError(
246
+ f"[{args.id}] {name} is a {type_name}: only a SCHEDULED_ACTION runs "
247
+ "through AGENDA",
248
+ "WRONG_TYPE",
249
+ hint=(
250
+ f"run an ACTION with: vortex object run {args.id} --app-id "
251
+ f"{args.app_id}"
252
+ if design_type == DesignType.ACTION
253
+ else None
254
+ ),
255
+ )
256
+ group = text(app.get("appgroup"))
257
+ path = f"/{group + '/' if group else ''}{text(app.get('appname'))}.pma/{name}"
258
+ sent = path + query
259
+ if "schedule=n" in text(row.get("options")).lower():
260
+ # AGENDA.refreshActionList skips 'Schedule=N' outright (empty Options
261
+ # load as on-demand instead), so --refresh can't help
262
+ raise CommandError(
263
+ f"{path} has Schedule=N (never), so AGENDA does not load it and would "
264
+ "ignore the run (silently) - nothing was sent",
265
+ "NOT_ON_AGENDA",
266
+ hint=(
267
+ f"give it a schedule first: vortex object update {args.id} "
268
+ f"--app-id {args.app_id} --schedule D --start-time 03:00 ..."
269
+ ),
270
+ )
271
+
272
+ items = parse_schedule(_console(ctx, SCHEDULE_CMD))
273
+ refreshed = False
274
+ if args.refresh and not _on_agenda(items, path):
275
+ _console(ctx, REFRESH_CMD)
276
+ refreshed = True
277
+ items = parse_schedule(_console(ctx, SCHEDULE_CMD))
278
+ if not _on_agenda(items, path):
279
+ raise CommandError(
280
+ f"{path} is not on AGENDA's list, so AGENDA would ignore the run "
281
+ "(silently) - nothing was sent",
282
+ "NOT_ON_AGENDA",
283
+ hint=(
284
+ "the application or its scheduled actions are disabled"
285
+ if refreshed
286
+ else "if it was created or renamed in the last 15 minutes, pass "
287
+ "--refresh; otherwise the application or its scheduled actions "
288
+ "are disabled"
289
+ ),
290
+ )
291
+ if any(
292
+ p.lower() == path.lower() for p in parse_waitlist(_console(ctx, WAITLIST_CMD))
293
+ ):
294
+ raise CommandError(
295
+ f"{path} is already waitlisted - AGENDA would skip the run",
296
+ "ALREADY_QUEUED",
297
+ )
298
+ others = [
299
+ i["path"]
300
+ for i in items
301
+ if sent.lower().startswith(i["path"].lower())
302
+ and i["path"].lower() != path.lower()
303
+ ]
304
+ if others and not args.force:
305
+ raise CommandError(
306
+ f"AGENDA matches by prefix: {sent} also matches {', '.join(others)}, "
307
+ f"so it would queue {name} {len(others) + 1} times - nothing was sent",
308
+ "PREFIX_COLLISION",
309
+ hint="pass --force to send it anyway",
310
+ )
311
+
312
+ command = RUN_CMD % sent
313
+ _console(ctx, command)
314
+ return {
315
+ "designbucketid": row.get("designbucketid"),
316
+ "name": name,
317
+ "path": sent,
318
+ "command": command,
319
+ "agendaRefreshed": refreshed,
320
+ "queued": len(others) + 1,
321
+ }
322
+
323
+
324
+ def _on_agenda(items: list[dict[str, Any]], path: str) -> bool:
325
+ return any(i["path"].lower() == path.lower() for i in items)
326
+
327
+
328
+ def _render_run(ctx: Context, args: argparse.Namespace, data: Any) -> None:
329
+ times = f" {data['queued']} times" if data["queued"] > 1 else ""
330
+ print(
331
+ f"Queued {data['path']} with AGENDA{times}: it runs as the system session "
332
+ "when a slot is free.\nFollow it with: vortex agenda -s "
333
+ f"{ctx.server.name} / vortex log -s {ctx.server.name}"
334
+ )
335
+
336
+
337
+ def _configure_run(parser: argparse.ArgumentParser) -> None:
338
+ parser.add_argument("id", metavar="ID", type=cli.positive_id)
339
+ parser.add_argument(
340
+ "--app-id",
341
+ metavar="ID",
342
+ type=cli.positive_id,
343
+ required=True,
344
+ help="The scheduled action's application (numeric)",
345
+ )
346
+ parser.add_argument(
347
+ "--param",
348
+ metavar="KEY=VALUE",
349
+ action="append",
350
+ default=[],
351
+ help="Pass a parameter to the action (repeatable): ...Name?&KEY=VALUE",
352
+ )
353
+ parser.add_argument(
354
+ "--refresh",
355
+ action="store_true",
356
+ help="Reread AGENDA's list first when the action is not on it",
357
+ )
358
+ parser.add_argument(
359
+ "--force",
360
+ action="store_true",
361
+ help="Send even when another action's path is a prefix of this one",
362
+ )
363
+
364
+
365
+ COMMANDS = [
366
+ Command(
367
+ "agenda",
368
+ "list",
369
+ "The scheduled actions AGENDA has loaded, by next run",
370
+ _list,
371
+ routes=(registry.CONSOLE,),
372
+ configure=_configure_list,
373
+ render=_render_list,
374
+ note="--app-id also reads the inventory",
375
+ description=(
376
+ "The scheduled actions AGENDA has loaded ('tell agenda schedule'),\n"
377
+ "sorted by next run, with the waitlist and AGENDA's counts. Only\n"
378
+ "what AGENDA loaded: an action in a disabled app, in an app with\n"
379
+ "scheduled actions disabled, or created since the last refresh is\n"
380
+ "not listed. 'vortex agenda' alone is 'vortex agenda list'."
381
+ ),
382
+ ),
383
+ Command(
384
+ "agenda",
385
+ "run",
386
+ "Queue a scheduled action with AGENDA now",
387
+ _run,
388
+ routes=(registry.INVENTORY, registry.DESIGN_GET, registry.CONSOLE),
389
+ write=True,
390
+ configure=_configure_run,
391
+ render=_render_run,
392
+ description=(
393
+ "Queue a SCHEDULED_ACTION with AGENDA now ('tell agenda run\n"
394
+ "/group/app.pma/Name'). It runs as the system session when a slot\n"
395
+ "is free; its result is only in 'vortex log'. Checked first,\n"
396
+ "because AGENDA answers nothing: refused when the action is not on\n"
397
+ "AGENDA's list (NOT_ON_AGENDA; --refresh rereads it), already\n"
398
+ "waitlisted (ALREADY_QUEUED), or would also match another action\n"
399
+ "by prefix and run twice (PREFIX_COLLISION; --force sends it).\n"
400
+ "ACTIONs are run with 'vortex object run'."
401
+ ),
402
+ ),
403
+ ]
@@ -17,7 +17,6 @@ from vortex.workspace import Workspace
17
17
  logger = logging.getLogger("vortex")
18
18
 
19
19
  REFRESH_APPLICATION_CMD = "tell agenda run /%s/RefreshDesign?&AppID=%d"
20
- RUN_CMD = "tell agenda run /%s"
21
20
  # The console shortcuts - the exact strings the addins match
22
21
  # (AGENDA.java tell(): "schedule", "refresh"; HTTP.java tell(): "cache flush"
23
22
  # only - 'tell http flush cache' silently does nothing)
@@ -56,11 +55,6 @@ def refresh_design_command(server: PuakmaServer, app_id: int) -> str:
56
55
  return REFRESH_APPLICATION_CMD % (server.webdesign_path, app_id)
57
56
 
58
57
 
59
- def run_command(server_path: str) -> str:
60
- """'tell agenda run /group/app.pma/action' for a design's server path."""
61
- return RUN_CMD % server_path.lstrip("/")
62
-
63
-
64
58
  def resolve_command(words: list[str]) -> str:
65
59
  """The console command for 'vortex execute WORDS...': a shortcut, or the words."""
66
60
  command = " ".join(words).strip()
@@ -1073,49 +1073,80 @@ def _add_content(parser: argparse.ArgumentParser) -> None:
1073
1073
 
1074
1074
  # -- run -------------------------------------------------------------------
1075
1075
 
1076
- _RUNNABLE = (DesignType.ACTION, DesignType.SCHEDULED_ACTION)
1076
+ _RUN_REFUSAL_HELP = {
1077
+ "NOT_EXPOSED": (
1078
+ "this server's gateway has no run route - upgrade vortex/gateway "
1079
+ "(object run no longer goes through the console)"
1080
+ ),
1081
+ "NOT_FOUND": "the server has no compiled ACTION at that path",
1082
+ }
1077
1083
 
1078
1084
 
1079
- def _run(ctx: Context, args: argparse.Namespace) -> dict[str, Any]:
1080
- from vortex.commands import execute
1085
+ def _run(ctx: Context, args: argparse.Namespace) -> dict[str, Any] | Outcome:
1081
1086
  from vortex.commands.app import find_app_row
1087
+ from vortex.webdesign import GatewayClient
1088
+ from vortex.webdesign import GatewayRefused
1089
+ from vortex.webdesign import refusal_detail
1082
1090
 
1083
1091
  app = find_app_row(ctx, args.app_id)
1084
1092
  row = ctx.client.get_design(args.app_id, args.id)
1085
- try:
1086
- design_type = DesignType(to_int(row.get("designtype") or 0))
1087
- except ValueError:
1088
- design_type = DesignType.ERROR
1089
1093
  name = text(row.get("name"))
1090
- if design_type not in _RUNNABLE:
1094
+ design_type = to_int(row.get("designtype") or 0)
1095
+ if design_type != DesignType.ACTION:
1096
+ hint = None
1097
+ if design_type == DesignType.SCHEDULED_ACTION:
1098
+ hint = (
1099
+ "queue it with AGENDA: vortex agenda run "
1100
+ f"{args.id} --app-id {args.app_id}"
1101
+ )
1091
1102
  raise CommandError(
1092
1103
  f"[{args.id}] {name} is a {_type_name(row.get('designtype'))}: only "
1093
- "an ACTION or SCHEDULED_ACTION can be run",
1104
+ "an ACTION can be run",
1094
1105
  "WRONG_TYPE",
1106
+ hint=hint,
1095
1107
  )
1096
1108
  group = text(app.get("appgroup"))
1097
- server_path = f"{group + '/' if group else ''}{text(app.get('appname'))}.pma/{name}"
1098
- command = execute.run_command(server_path)
1099
- reply = execute.console_command(ctx.server, command)
1100
- return {
1109
+ path = f"/{group + '/' if group else ''}{text(app.get('appname'))}.pma/{name}"
1110
+ try:
1111
+ with ctx.server as s:
1112
+ reply = GatewayClient(s).run(path, reply=args.output)
1113
+ except GatewayRefused as e:
1114
+ raise CommandError(
1115
+ refusal_detail(e, _RUN_REFUSAL_HELP), e.error_code, role=e.role
1116
+ ) from e
1117
+ data = {
1101
1118
  "designbucketid": row.get("designbucketid"),
1102
1119
  "name": name,
1103
- "designtype": row.get("designtype"),
1104
- "path": f"/{server_path}",
1105
- "command": command,
1106
- "reply": reply or "",
1120
+ **{k: v for k, v in reply.items() if k != "ok"},
1107
1121
  }
1122
+ if data.get("exception"):
1123
+ return Outcome(
1124
+ data,
1125
+ CommandError(f"{path} threw: {data['exception']}", "ACTION_FAILED"),
1126
+ )
1127
+ return data
1108
1128
 
1109
1129
 
1110
1130
  def _render_run(ctx: Context, args: argparse.Namespace, data: Any) -> None:
1111
- print(f"Sent: {data['command']}")
1112
- if data["reply"]:
1113
- print(data["reply"])
1131
+ seconds = to_int(data.get("durationMs") or 0) / 1000
1132
+ if data.get("exception"):
1133
+ print(f"Ran {data['path']}: it threw after {seconds:.2f}s")
1134
+ return
1135
+ print(f"Ran {data['path']} in {seconds:.2f}s")
1136
+ if data.get("redirectTo"):
1137
+ print(f"Redirect: {data['redirectTo']}")
1138
+ if args.output and data.get("body"):
1139
+ print(data["body"])
1114
1140
 
1115
1141
 
1116
1142
  def _configure_run(parser: argparse.ArgumentParser) -> None:
1117
1143
  parser.add_argument("id", metavar="ID", type=cli.positive_id)
1118
1144
  _add_app_id(parser)
1145
+ parser.add_argument(
1146
+ "--output",
1147
+ action="store_true",
1148
+ help="Also print the action's reply (its buffer, as text)",
1149
+ )
1119
1150
 
1120
1151
 
1121
1152
  def _configure_get(parser: argparse.ArgumentParser) -> None:
@@ -1245,17 +1276,18 @@ COMMANDS = [
1245
1276
  Command(
1246
1277
  "object",
1247
1278
  "run",
1248
- "Run an action or scheduled action now (AGENDA 'tell agenda run')",
1279
+ "Run an action now and wait for it (the gateway's POST run)",
1249
1280
  _run,
1250
- routes=(registry.INVENTORY, registry.DESIGN_GET, registry.CONSOLE),
1281
+ routes=(registry.INVENTORY, registry.DESIGN_GET, registry.RUN),
1251
1282
  write=True,
1252
1283
  configure=_configure_run,
1253
1284
  render=_render_run,
1254
1285
  description=(
1255
- "Run an ACTION or SCHEDULED_ACTION now: sends\n"
1256
- "'tell agenda run /group/app.pma/name' to the console (the gateway's\n"
1257
- "POST console needs GatewaySystem). Check 'vortex log' for what\n"
1258
- "the action did."
1286
+ "Run an ACTION now, as this identity, and wait for it to finish:\n"
1287
+ "the gateway's POST run (needs GatewaySystem) loads and executes\n"
1288
+ "the action and reports how long it took, its redirect and reply\n"
1289
+ "size - and the exception if it threw (exit 1). --output prints\n"
1290
+ "the action's reply. Scheduled actions are not run here."
1259
1291
  ),
1260
1292
  ),
1261
1293
  ]
@@ -84,6 +84,8 @@ STUBS = {
84
84
  REMOVED_IN = {"config": "8.1", "find": "8.1", "grep": "8.1", "libs": "8.1"}
85
85
  # Old names that still work, quietly: rewritten before parsing
86
86
  ALIASES = {"use": ["server", "use"], "ls": ["app", "list", "--local"]}
87
+ # Nouns that run a verb when given none ('vortex agenda' = 'vortex agenda list')
88
+ DEFAULT_VERBS = {"agenda": "list"}
87
89
  # 7.x forms of commands whose NAME 8.0 reuses as a noun
88
90
  OLD_FORMS = {
89
91
  "db": (
@@ -115,7 +117,14 @@ def _command_index(tokens: Sequence[str]) -> int | None:
115
117
 
116
118
  def _apply_aliases(argv: list[str]) -> list[str]:
117
119
  i = _command_index(argv)
118
- if i is None or argv[i] not in ALIASES:
120
+ if i is None:
121
+ return argv
122
+ if argv[i] in DEFAULT_VERBS:
123
+ rest = argv[i + 1 :]
124
+ if not rest or (rest[0].startswith("-") and rest[0] not in ("-h", "--help")):
125
+ return [*argv[: i + 1], DEFAULT_VERBS[argv[i]], *rest]
126
+ return argv
127
+ if argv[i] not in ALIASES:
119
128
  return argv
120
129
  return [*argv[:i], *ALIASES[argv[i]], *argv[i + 1 :]]
121
130
 
@@ -70,6 +70,7 @@ SYSTEMJAR = "GET /systemjar"
70
70
  WHOAMI = "GET /whoami"
71
71
  LOGS = "GET /logs"
72
72
  CONSOLE = "POST /console"
73
+ RUN = "POST /run"
73
74
  EXPORT = "GET /export"
74
75
  IMPORT = "POST /import"
75
76
 
@@ -78,6 +79,7 @@ NOUN_HELP = {
78
79
  "object": "Design objects: list, get, grep, create, update, diff, copy, delete, run",
79
80
  "keyword": "Application keywords: list, get, set, delete",
80
81
  "db": "Databases: connections, SQL queries and the data dictionary",
82
+ "agenda": "Scheduled actions (AGENDA): list, run",
81
83
  }
82
84
 
83
85
 
@@ -203,6 +205,7 @@ class Command:
203
205
 
204
206
 
205
207
  def all_commands() -> list[Command]:
208
+ from vortex.commands import agenda
206
209
  from vortex.commands import app
207
210
  from vortex.commands import db
208
211
  from vortex.commands import diff
@@ -219,6 +222,7 @@ def all_commands() -> list[Command]:
219
222
  *keyword.COMMANDS,
220
223
  *db.COMMANDS,
221
224
  *fetch.COMMANDS,
225
+ *agenda.COMMANDS,
222
226
  ]
223
227
 
224
228
 
@@ -24,10 +24,10 @@ webdesign's own. It adds three things:
24
24
  - Guards: no ids that belong to another application (NOT_FOUND), no
25
25
  system database (SYSTEM_DB), one DML/SELECT statement per SQL request
26
26
  (SQL_NOT_ALLOWED).
27
- - Its own routes on the same base URL: whoami, logs, console, export
28
- and import (GatewayClient). Without the gateway, whoami/logs/console/
29
- import do not exist (GatewayNotInstalled); export uses webdesign's
30
- ExportPMX.
27
+ - Its own routes on the same base URL: whoami, logs, console, run,
28
+ export and import (GatewayClient). Without the gateway, whoami/logs/
29
+ console/run/import do not exist (GatewayNotInstalled); export uses
30
+ webdesign's ExportPMX.
31
31
 
32
32
  Every failure carries a machine-readable code (`.code`): the gateway's
33
33
  own refusal code as sent, or one the CLI sets - NOT_FOUND (404),
@@ -110,6 +110,8 @@ _ZIP_MAGIC = b"PK\x03\x04"
110
110
  DOWNLOAD_TIMEOUT_SECONDS = 120
111
111
  UPDATE_TIMEOUT_SECONDS = 60
112
112
  REQUEST_TIMEOUT_SECONDS = 30
113
+ # object run waits for the action to finish (prod's Cloudflare still cuts at ~100s)
114
+ RUN_TIMEOUT_SECONDS = 300
113
115
 
114
116
  _ROUTE_MISSING = "Api does not exist"
115
117
 
@@ -775,7 +777,7 @@ class WhoAmI(NamedTuple):
775
777
  class GatewayClient(WebDesignClient):
776
778
  """
777
779
  The gateway's local routes (never forwarded to webdesign): whoami, logs,
778
- console, export and import. On a server with a blank gateway_path they
780
+ console, run, export and import. On a server with a blank gateway_path they
779
781
  raise GatewayNotInstalled - except export, which goes to webdesign's
780
782
  ExportPMX instead.
781
783
  """
@@ -831,6 +833,19 @@ class GatewayClient(WebDesignClient):
831
833
  reply = _as_dict(self._request("POST", "console", {"cmd": cmd}))
832
834
  return str(reply.get("reply") or "")
833
835
 
836
+ def run(self, path: str, reply: bool = False) -> dict[str, Any]:
837
+ """
838
+ POST run (GatewaySystem): runs the ACTION at path
839
+ (/group/app.pma/Name) now, as this identity, and waits for it. The
840
+ reply is {ok, path, durationMs, redirectTo, contentType,
841
+ httpReplyCode, size}, plus body (the action's reply as text) when
842
+ reply is True, or exception when the action threw. A path that is
843
+ not a compiled ACTION is NOT_FOUND.
844
+ """
845
+ self._require_gateway("'vortex object run'")
846
+ body = {"path": path, "reply": "1" if reply else ""}
847
+ return _as_dict(self._request("POST", "run", body, timeout=RUN_TIMEOUT_SECONDS))
848
+
834
849
  def get_last_log_items(
835
850
  self,
836
851
  limit_items: int,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vortex_cli
3
- Version: 8.1.1
3
+ Version: 8.2.0
4
4
  Summary: Vortex CLI
5
5
  Author-email: Jordan Amos <jordan.amos@gmail.com>
6
6
  License: MIT License
@@ -106,6 +106,18 @@ While it is possible to use without it, this software has been purposefully desi
106
106
  java_environment_name = JavaSE-17 ; Java Execution Environment name https://docs.osgi.org/reference/eenames.html
107
107
  ```
108
108
 
109
+ ## Upgrading to 8.2
110
+
111
+ - **`object run` runs ACTIONs, and waits.** It calls the gateway's new `POST run` (gateway
112
+ `AppVersion` 0.1.4 or later) instead of sending `tell agenda run` to the console, and
113
+ reports how long the action took, its reply and any exception (see
114
+ [Running an action](#running-an-action)). An older gateway refuses it with `NOT_EXPOSED`.
115
+ It no longer accepts a SCHEDULED_ACTION.
116
+ - **`vortex agenda`** lists the scheduled actions AGENDA has loaded and queues one with
117
+ `agenda run`, refusing the cases where AGENDA would silently do nothing (see
118
+ [The agenda](#the-agenda)). It replaces both `object run` on a scheduled action and
119
+ `vortex execute tell agenda run ...`.
120
+
109
121
  ## Upgrading to 8.1
110
122
 
111
123
  8.1 does two things. It replaces `vortex config` with plain top-level commands that read
@@ -289,7 +301,7 @@ vortex object list [QUERY] [--app-id ID [--local]] [--strict --inherits-from|-
289
301
  --ids-only --show-params --type T...] [--json]
290
302
  vortex object grep PATTERN [--app-id ID] [--output-paths|--output-apps]
291
303
  [--include-resources|--type T...] [--json]
292
- vortex object run ID --app-id ID
304
+ vortex object run ID --app-id ID [--output] [--json]
293
305
  vortex object delete ID... --app-id ID [--yes]
294
306
 
295
307
  ── keyword ──────────────────────────────────────────────────────────────────
@@ -315,6 +327,10 @@ vortex db update-column DB TABLE COLUMN --app-id ID [--name --type --size --desc
315
327
  --cascade-delete|--no-cascade-delete]
316
328
  vortex db delete-column DB TABLE COLUMN --app-id ID [--yes]
317
329
 
330
+ ── agenda ───────────────────────────────────────────────────────────────────
331
+ vortex agenda [list] [--app-id ID] [--json]
332
+ vortex agenda run ID --app-id ID [--param KEY=VALUE ...] [--refresh --force] [--json]
333
+
318
334
  ── Workspace ────────────────────────────────────────────────────────────────
319
335
  vortex ls [app list filters] [--json] = app list --local
320
336
  vortex clone APP... [--group --reclone --all --get-resources --open-urls --timeout]
@@ -412,7 +428,7 @@ addins match:
412
428
 
413
429
  | Shortcut | Sends | Effect |
414
430
  |---|---|---|
415
- | `schedule` | `tell agenda schedule` | lists every scheduled action and its next run |
431
+ | `schedule` | `tell agenda schedule` | lists every scheduled action and its next run (raw text; `vortex agenda` parses it) |
416
432
  | `refresh-agenda` | `tell agenda refresh` | AGENDA rereads every schedule now |
417
433
  | `refresh-design APP_ID` | `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N` | rebuilds the application's design from its template, as `app refresh` does (a clone is then out of date) |
418
434
  | `flush-cache` | `tell http cache flush` | clears the design cache and all action class loaders |
@@ -420,12 +436,28 @@ addins match:
420
436
  `tell http flush cache` is **not** a valid command: the HTTP addin only matches
421
437
  `cache flush`, and anything else does nothing, silently. Use `flush-cache`.
422
438
 
423
- Two console operations act on one application or object, so they also live with those nouns:
424
-
425
- | Command | Sends |
426
- |---|---|
427
- | `vortex app refresh APP_ID` | `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N` - rebuilds the design from its template (a clone is then out of date) |
428
- | `vortex object run ID --app-id N` | `tell agenda run /group/app.pma/name` - runs an ACTION or SCHEDULED_ACTION now |
439
+ `vortex app refresh APP_ID` also lives with its noun: it sends
440
+ `tell agenda run /<webdesign_path>/RefreshDesign?&AppID=N`, which rebuilds the design from its
441
+ template (a clone is then out of date).
442
+
443
+ ### Running an action
444
+
445
+ `vortex object run ID --app-id N` runs an ACTION now, as your identity, and waits for it:
446
+ the gateway's `POST run` (`GatewaySystem`) loads the action's class, executes it and replies
447
+ with how long it took, its redirect, content type and reply size. `--output` also prints the
448
+ action's reply (its buffer, as text). An action that throws exits 1 with `ACTION_FAILED` and
449
+ the exception; a path with no compiled ACTION is `NOT_FOUND`.
450
+
451
+ - **ACTIONs only.** A SCHEDULED_ACTION (or any other type) is `WRONG_TYPE`. Scheduled
452
+ actions are queued with AGENDA: `vortex agenda run ID --app-id N` ([The agenda](#the-agenda)).
453
+ - **No client connection.** The action's `getOutputStream()` is null, as under AGENDA, so it
454
+ must buffer its reply (`write`/`setBuffer`); one that streams unconditionally fails with
455
+ the NullPointerException as its result.
456
+ - **Synchronous.** The CLI waits up to 300 s. Behind Cloudflare (prod) the request is cut
457
+ at about 100 s; the action still finishes on the server - check `vortex log`.
458
+ - **Gateway required.** It needs a gateway with the `run` route; an older gateway refuses with
459
+ `NOT_EXPOSED`. Earlier versions of `object run` sent `tell agenda run`, which only reached
460
+ scheduled actions on AGENDA's list and did nothing, silently, for an ACTION.
429
461
 
430
462
  The 8.0 flags (`--show-schedule`, `--refresh-agenda`, `--flush-cache`, `--refresh-design`,
431
463
  `--run`) were removed in 8.1. `app refresh` was `app refresh-design` in 8.1.0; the old
@@ -573,6 +605,40 @@ vortex object get 7901 --app-id 62 # the parsed schedule
573
605
  starts; if that lands between the command's read and write, the old `LastRun` is written
574
606
  back and the action may run one interval early.
575
607
 
608
+ ### The agenda
609
+
610
+ AGENDA has no API beyond its console commands, so `vortex agenda` sends them through the same
611
+ console route as `vortex execute` (the gateway's `POST console`, `GatewaySystem`, or the SOAP
612
+ console without the gateway) and parses the text they answer.
613
+
614
+ ```
615
+ vortex agenda -s dev # = agenda list
616
+ vortex agenda list --app-id 9 -s dev
617
+ vortex agenda run 7825 --app-id 60 -s dev [--param AppID=12] [--refresh] [--force]
618
+ ```
619
+
620
+ - **`list`** - `tell agenda schedule`: every scheduled action AGENDA has **loaded**, by next
621
+ run (server time), with the waitlist and AGENDA's counts (running, waitlisted, scheduled,
622
+ max at once). An action with empty `Options` is loaded as *on demand only*. Not listed: an
623
+ action with `Schedule=N`, one in a disabled application or an application with scheduled
624
+ actions disabled, and one created or renamed since AGENDA's last refresh (every 15
625
+ minutes). AGENDA says how many actions are running, never which.
626
+ - **`run`** - queues a SCHEDULED_ACTION: `tell agenda run /group/app.pma/Name[?&KEY=VALUE...]`.
627
+ It runs as the system session when one of AGENDA's slots is free; its result is only in
628
+ `vortex log`. AGENDA answers that command with nothing, whatever it did, so `run` checks
629
+ first and sends nothing when AGENDA would not run it:
630
+ - `NOT_ON_AGENDA` - the action has `Schedule=N` (give it a schedule first), or it is not on
631
+ the loaded list. `--refresh` sends `tell agenda refresh` and checks again (for an action
632
+ created or renamed in the last 15 minutes).
633
+ - `ALREADY_QUEUED` - it is already waitlisted; AGENDA would skip it.
634
+ - `PREFIX_COLLISION` - AGENDA matches the path by prefix and queues it once per matching
635
+ item, so `.../SyncAll` would also match an item `.../Sync` and run twice. `--force` sends
636
+ it anyway.
637
+ - `WRONG_TYPE` - not a SCHEDULED_ACTION (an ACTION is run with `vortex object run`).
638
+ - A queued run moves the action's next scheduled run (AGENDA resets it), and it can't be
639
+ stopped from here (`vortex execute tell agenda stop /group/app.pma/Name` asks the action to
640
+ quit; it has to check for that itself).
641
+
576
642
  ### Keyword values and secrets
577
643
 
578
644
  Keywords are an application's live configuration, and in practice they hold credentials in
@@ -22,6 +22,7 @@ vortex/util.py
22
22
  vortex/webdesign.py
23
23
  vortex/workspace.py
24
24
  vortex/commands/__init__.py
25
+ vortex/commands/agenda.py
25
26
  vortex/commands/app.py
26
27
  vortex/commands/clean.py
27
28
  vortex/commands/clone.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes