systemlink-cli 1.3.1__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.
Files changed (74) hide show
  1. slcli/__init__.py +1 -0
  2. slcli/__main__.py +23 -0
  3. slcli/_version.py +4 -0
  4. slcli/asset_click.py +1289 -0
  5. slcli/cli_formatters.py +218 -0
  6. slcli/cli_utils.py +504 -0
  7. slcli/comment_click.py +602 -0
  8. slcli/completion_click.py +418 -0
  9. slcli/config.py +81 -0
  10. slcli/config_click.py +498 -0
  11. slcli/dff_click.py +979 -0
  12. slcli/dff_decorators.py +24 -0
  13. slcli/example_click.py +404 -0
  14. slcli/example_loader.py +274 -0
  15. slcli/example_provisioner.py +2777 -0
  16. slcli/examples/README.md +134 -0
  17. slcli/examples/_schema/schema-v1.0.json +169 -0
  18. slcli/examples/demo-complete-workflow/README.md +323 -0
  19. slcli/examples/demo-complete-workflow/config.yaml +638 -0
  20. slcli/examples/demo-test-plans/README.md +132 -0
  21. slcli/examples/demo-test-plans/config.yaml +154 -0
  22. slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
  23. slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
  24. slcli/examples/exercise-7-1-test-plans/README.md +93 -0
  25. slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
  26. slcli/examples/spec-compliance-notebooks/README.md +140 -0
  27. slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
  28. slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
  29. slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
  30. slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
  31. slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  32. slcli/feed_click.py +892 -0
  33. slcli/file_click.py +932 -0
  34. slcli/function_click.py +1400 -0
  35. slcli/function_templates.py +85 -0
  36. slcli/main.py +406 -0
  37. slcli/mcp_click.py +269 -0
  38. slcli/mcp_server.py +748 -0
  39. slcli/notebook_click.py +1770 -0
  40. slcli/platform.py +345 -0
  41. slcli/policy_click.py +679 -0
  42. slcli/policy_utils.py +411 -0
  43. slcli/profiles.py +411 -0
  44. slcli/response_handlers.py +359 -0
  45. slcli/routine_click.py +763 -0
  46. slcli/skill_click.py +253 -0
  47. slcli/skills/slcli/SKILL.md +713 -0
  48. slcli/skills/slcli/references/analysis-recipes.md +474 -0
  49. slcli/skills/slcli/references/filtering.md +236 -0
  50. slcli/skills/systemlink-webapp/SKILL.md +744 -0
  51. slcli/skills/systemlink-webapp/references/deployment.md +123 -0
  52. slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
  53. slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
  54. slcli/ssl_trust.py +93 -0
  55. slcli/system_click.py +2216 -0
  56. slcli/table_utils.py +124 -0
  57. slcli/tag_click.py +794 -0
  58. slcli/templates_click.py +599 -0
  59. slcli/testmonitor_click.py +1667 -0
  60. slcli/universal_handlers.py +305 -0
  61. slcli/user_click.py +1218 -0
  62. slcli/utils.py +832 -0
  63. slcli/web_editor.py +295 -0
  64. slcli/webapp_click.py +981 -0
  65. slcli/workflow_preview.py +287 -0
  66. slcli/workflows_click.py +988 -0
  67. slcli/workitem_click.py +2258 -0
  68. slcli/workspace_click.py +576 -0
  69. slcli/workspace_utils.py +206 -0
  70. systemlink_cli-1.3.1.dist-info/METADATA +20 -0
  71. systemlink_cli-1.3.1.dist-info/RECORD +74 -0
  72. systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
  73. systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
  74. systemlink_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
slcli/routine_click.py ADDED
@@ -0,0 +1,763 @@
1
+ """CLI commands for managing SystemLink Routines.
2
+
3
+ Supports two API versions:
4
+ v1 (niroutine/v1): Notebook-execution routines with SCHEDULED or TRIGGERED types.
5
+ v2 (niroutine/v2): General event-action routines supporting any event/action types.
6
+ """
7
+
8
+ import json
9
+ import sys
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ import click
13
+ import questionary
14
+ import requests
15
+
16
+ from .utils import (
17
+ ExitCodes,
18
+ format_success,
19
+ get_base_url,
20
+ get_workspace_map,
21
+ handle_api_error,
22
+ make_api_request,
23
+ )
24
+ from .workspace_utils import (
25
+ filter_by_workspace,
26
+ get_effective_workspace,
27
+ get_workspace_display_name,
28
+ resolve_workspace_filter,
29
+ )
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Helpers
34
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ def _routine_base_url(api_version: str) -> str:
38
+ """Return the base URL for routines based on the API version.
39
+
40
+ Args:
41
+ api_version: Either 'v1' or 'v2'.
42
+
43
+ Returns:
44
+ The base URL string for the routines endpoint.
45
+ """
46
+ return f"{get_base_url()}/niroutine/{api_version}/routines"
47
+
48
+
49
+ def _parse_json_option(value: Optional[str], field_name: str) -> Any:
50
+ """Parse a JSON string option, exiting with an error on invalid JSON.
51
+
52
+ Args:
53
+ value: JSON string value, or None.
54
+ field_name: Name of the CLI option, used in error messages.
55
+
56
+ Returns:
57
+ Parsed Python object, or None if value is None.
58
+ """
59
+ if value is None:
60
+ return None
61
+ try:
62
+ return json.loads(value)
63
+ except json.JSONDecodeError as exc:
64
+ click.echo(f"✗ Invalid JSON for --{field_name}: {exc}", err=True)
65
+ sys.exit(ExitCodes.INVALID_INPUT)
66
+
67
+
68
+ def _make_v1_formatter(
69
+ workspace_map: Dict[str, str],
70
+ ) -> Any:
71
+ """Create a v1 routine table row formatter with workspace name resolution.
72
+
73
+ Args:
74
+ workspace_map: Mapping of workspace ID to display name.
75
+
76
+ Returns:
77
+ Formatter function that accepts a routine dict and returns a row list.
78
+ """
79
+
80
+ def formatter(routine: Dict[str, Any]) -> List[str]:
81
+ enabled = "✓" if routine.get("enabled", False) else "✗"
82
+ ws_name = get_workspace_display_name(routine.get("workspace", ""), workspace_map)
83
+ return [
84
+ routine.get("name", ""),
85
+ routine.get("id", ""),
86
+ routine.get("type", ""),
87
+ enabled,
88
+ ws_name,
89
+ ]
90
+
91
+ return formatter
92
+
93
+
94
+ def _make_v2_formatter(
95
+ workspace_map: Dict[str, str],
96
+ ) -> Any:
97
+ """Create a v2 routine table row formatter with workspace name resolution.
98
+
99
+ Args:
100
+ workspace_map: Mapping of workspace ID to display name.
101
+
102
+ Returns:
103
+ Formatter function that accepts a routine dict and returns a row list.
104
+ """
105
+
106
+ def formatter(routine: Dict[str, Any]) -> List[str]:
107
+ enabled = "✓" if routine.get("enabled", False) else "✗"
108
+ event_type = (routine.get("event") or {}).get("type", "")
109
+ ws_name = get_workspace_display_name(routine.get("workspace", ""), workspace_map)
110
+ return [
111
+ routine.get("name", ""),
112
+ routine.get("id", ""),
113
+ event_type,
114
+ enabled,
115
+ ws_name,
116
+ ]
117
+
118
+ return formatter
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Command registration
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ def register_routine_commands(cli: Any) -> None:
127
+ """Register the 'routine' command group and its subcommands."""
128
+
129
+ @cli.group()
130
+ def routine() -> None:
131
+ """Manage SystemLink routines (v1: notebook scheduling, v2: event-action)."""
132
+ pass
133
+
134
+ # ------------------------------------------------------------------
135
+ # list
136
+ # ------------------------------------------------------------------
137
+
138
+ @routine.command(name="list")
139
+ @click.option(
140
+ "--api-version",
141
+ type=click.Choice(["v1", "v2"]),
142
+ default="v2",
143
+ show_default=True,
144
+ help="API version to use (v1: notebook routines, v2: event-action routines)",
145
+ )
146
+ @click.option(
147
+ "--format",
148
+ "-f",
149
+ "format_output",
150
+ type=click.Choice(["table", "json"]),
151
+ default="table",
152
+ show_default=True,
153
+ help="Output format",
154
+ )
155
+ @click.option(
156
+ "--take",
157
+ "-t",
158
+ type=int,
159
+ default=25,
160
+ show_default=True,
161
+ help="Maximum number of routines to return",
162
+ )
163
+ @click.option(
164
+ "--enabled",
165
+ "enabled_filter",
166
+ flag_value="enabled",
167
+ default=None,
168
+ help="Show only enabled routines",
169
+ )
170
+ @click.option(
171
+ "--disabled",
172
+ "enabled_filter",
173
+ flag_value="disabled",
174
+ help="Show only disabled routines",
175
+ )
176
+ @click.option(
177
+ "--workspace",
178
+ "-w",
179
+ "workspace_filter",
180
+ type=str,
181
+ default=None,
182
+ help="Filter by workspace name or ID",
183
+ )
184
+ @click.option(
185
+ "--type",
186
+ "routine_type",
187
+ type=click.Choice(["TRIGGERED", "SCHEDULED"]),
188
+ default=None,
189
+ help="Filter by routine type (v1 only)",
190
+ )
191
+ @click.option(
192
+ "--event-type",
193
+ "event_type",
194
+ type=str,
195
+ default=None,
196
+ help="Filter by event type (v2 only)",
197
+ )
198
+ @click.option(
199
+ "--filter",
200
+ "name_filter",
201
+ type=str,
202
+ default=None,
203
+ help="Filter by routine name (case-insensitive substring match)",
204
+ )
205
+ def list_routines(
206
+ api_version: str,
207
+ format_output: str,
208
+ take: int,
209
+ enabled_filter: Optional[str],
210
+ routine_type: Optional[str],
211
+ event_type: Optional[str],
212
+ name_filter: Optional[str],
213
+ workspace_filter: Optional[str],
214
+ ) -> None:
215
+ """List routines with optional filtering.
216
+
217
+ By default all routines (enabled and disabled) are returned. Use
218
+ --enabled or --disabled to narrow results. The --filter option performs
219
+ case-insensitive substring matching on routine names. The --workspace
220
+ option accepts a workspace name or ID. The --take option controls items
221
+ per page in table mode or the maximum items returned in JSON mode.
222
+ """
223
+ try:
224
+ url = _routine_base_url(api_version)
225
+ params: List[str] = []
226
+
227
+ if enabled_filter == "enabled":
228
+ params.append("Enabled=true")
229
+ elif enabled_filter == "disabled":
230
+ params.append("Enabled=false")
231
+
232
+ if api_version == "v1" and routine_type:
233
+ params.append(f"Type={routine_type}")
234
+
235
+ if api_version == "v2" and event_type:
236
+ params.append(f"EventType={event_type}")
237
+
238
+ if params:
239
+ url = url + "?" + "&".join(params)
240
+
241
+ resp = make_api_request("GET", url, payload=None)
242
+ data = resp.json()
243
+
244
+ routines: List[Dict[str, Any]] = data.get("routines", [])
245
+
246
+ # Resolve workspace map once for both filtering and display
247
+ try:
248
+ workspace_map: Dict[str, str] = get_workspace_map()
249
+ except Exception:
250
+ workspace_map = {}
251
+
252
+ # Client-side workspace filter (APIs don't support it as a query param)
253
+ workspace_filter = get_effective_workspace(workspace_filter)
254
+ if workspace_filter:
255
+ resolved_ws = resolve_workspace_filter(workspace_filter, workspace_map)
256
+ routines = filter_by_workspace(routines, resolved_ws, workspace_map)
257
+
258
+ # Client-side name filter (APIs don't support name filtering)
259
+ if name_filter:
260
+ lower_filter = name_filter.lower()
261
+ routines = [r for r in routines if lower_filter in r.get("name", "").lower()]
262
+
263
+ if format_output == "json":
264
+ # Apply take limit then output all at once without pagination
265
+ if take > 0:
266
+ routines = routines[:take]
267
+ click.echo(json.dumps(routines, indent=2))
268
+ return
269
+
270
+ if not routines:
271
+ click.echo("No routines found.")
272
+ return
273
+
274
+ from .table_utils import output_formatted_list
275
+
276
+ if api_version == "v1":
277
+ headers = ["Name", "ID", "Type", "Enabled", "Workspace"]
278
+ widths = [30, 36, 12, 8, 30]
279
+ formatter = _make_v1_formatter(workspace_map)
280
+ else:
281
+ headers = ["Name", "ID", "Event Type", "Enabled", "Workspace"]
282
+ widths = [30, 36, 15, 8, 30]
283
+ formatter = _make_v2_formatter(workspace_map)
284
+
285
+ # Interactive pagination: show `take` items per page
286
+ total = len(routines)
287
+ offset = 0
288
+
289
+ while True:
290
+ page = routines[offset : offset + take]
291
+ output_formatted_list(
292
+ page,
293
+ format_output,
294
+ headers,
295
+ widths,
296
+ formatter,
297
+ "",
298
+ "routine(s)",
299
+ )
300
+
301
+ offset += len(page)
302
+
303
+ if offset >= total:
304
+ break
305
+
306
+ click.echo(f"\nShowing {offset} of {total} routine(s).")
307
+
308
+ try:
309
+ is_tty = sys.stdout.isatty() and sys.stdin.isatty()
310
+ except (OSError, AttributeError):
311
+ is_tty = False
312
+
313
+ if not is_tty:
314
+ break
315
+
316
+ if not questionary.confirm("Show next page?", default=True).ask():
317
+ break
318
+
319
+ except Exception as exc:
320
+ handle_api_error(exc)
321
+
322
+ # ------------------------------------------------------------------
323
+ # get
324
+ # ------------------------------------------------------------------
325
+
326
+ @routine.command(name="get")
327
+ @click.argument("routine_id")
328
+ @click.option(
329
+ "--api-version",
330
+ type=click.Choice(["v1", "v2"]),
331
+ default="v2",
332
+ show_default=True,
333
+ help="API version to use",
334
+ )
335
+ @click.option(
336
+ "--format",
337
+ "-f",
338
+ "format_output",
339
+ type=click.Choice(["table", "json"]),
340
+ default="json",
341
+ show_default=True,
342
+ help="Output format",
343
+ )
344
+ def get_routine(routine_id: str, api_version: str, format_output: str) -> None:
345
+ """Get a routine by ID.
346
+
347
+ ROUTINE_ID is the unique identifier of the routine to retrieve.
348
+ """
349
+ try:
350
+ url = f"{_routine_base_url(api_version)}/{routine_id}"
351
+ resp = make_api_request("GET", url, payload=None)
352
+ routine = resp.json()
353
+
354
+ if format_output == "json":
355
+ click.echo(json.dumps(routine, indent=2))
356
+ return
357
+
358
+ from .table_utils import output_formatted_list
359
+
360
+ # Only reached for table output — the json branch returned early above.
361
+ # Fetch workspace_map here so it is never called for json requests.
362
+ try:
363
+ workspace_map: Dict[str, str] = get_workspace_map()
364
+ except Exception:
365
+ click.echo(
366
+ "✗ Warning: Unable to load workspace mapping; "
367
+ "workspace names will not be shown.",
368
+ err=True,
369
+ )
370
+ workspace_map = {}
371
+
372
+ if api_version == "v1":
373
+ headers = ["Name", "ID", "Type", "Enabled", "Workspace"]
374
+ widths = [30, 36, 12, 8, 30]
375
+ formatter = _make_v1_formatter(workspace_map)
376
+ else:
377
+ headers = ["Name", "ID", "Event Type", "Enabled", "Workspace"]
378
+ widths = [30, 36, 15, 8, 30]
379
+ formatter = _make_v2_formatter(workspace_map)
380
+
381
+ output_formatted_list(
382
+ [routine],
383
+ format_output,
384
+ headers,
385
+ widths,
386
+ formatter,
387
+ "",
388
+ "routine(s)",
389
+ )
390
+
391
+ except Exception as exc:
392
+ handle_api_error(exc)
393
+
394
+ # ------------------------------------------------------------------
395
+ # create
396
+ # ------------------------------------------------------------------
397
+
398
+ @routine.command(name="create")
399
+ @click.option(
400
+ "--api-version",
401
+ type=click.Choice(["v1", "v2"]),
402
+ default="v2",
403
+ show_default=True,
404
+ help="API version to use",
405
+ )
406
+ @click.option("--name", "-n", type=str, required=True, help="Name of the routine")
407
+ @click.option("--description", "-d", type=str, default=None, help="Description of the routine")
408
+ @click.option("--workspace", "-w", type=str, default=None, help="Workspace ID")
409
+ @click.option(
410
+ "--enabled/--disabled",
411
+ "enabled",
412
+ default=False,
413
+ show_default=True,
414
+ help="Enable or disable the routine upon creation",
415
+ )
416
+ # v1-specific options
417
+ @click.option(
418
+ "--type",
419
+ "routine_type",
420
+ type=click.Choice(["TRIGGERED", "SCHEDULED"]),
421
+ default=None,
422
+ help="Routine type (v1 only, required for v1)",
423
+ )
424
+ @click.option(
425
+ "--notebook-id",
426
+ type=str,
427
+ default=None,
428
+ help="Notebook ID to execute (v1 only, required for v1)",
429
+ )
430
+ @click.option(
431
+ "--trigger",
432
+ "trigger_json",
433
+ type=str,
434
+ default=None,
435
+ help=(
436
+ "Trigger definition as JSON string (v1 TRIGGERED type). "
437
+ 'E.g. \'{"source":"FILES","events":["CREATED"],"filter":"extension=\\".csv\\""}\''
438
+ ),
439
+ )
440
+ @click.option(
441
+ "--schedule",
442
+ "schedule_json",
443
+ type=str,
444
+ default=None,
445
+ help=(
446
+ "Schedule definition as JSON string (v1 SCHEDULED type). "
447
+ 'E.g. \'{"startTime":"2026-01-01T00:00:00Z","repeat":"HOUR"}\''
448
+ ),
449
+ )
450
+ # v2-specific options
451
+ @click.option(
452
+ "--event",
453
+ "event_json",
454
+ type=str,
455
+ default=None,
456
+ help=(
457
+ "Event definition as JSON string (v2 only, required for v2). "
458
+ 'E.g. \'{"type":"tag","triggers":[{"name":"cond","configuration":{}}]}\''
459
+ ),
460
+ )
461
+ @click.option(
462
+ "--actions",
463
+ "actions_json",
464
+ type=str,
465
+ default=None,
466
+ help=(
467
+ "Actions as a JSON array string (v2 only, required for v2). "
468
+ 'E.g. \'[{"type":"alarm","configuration":{"severity":1}}]\''
469
+ ),
470
+ )
471
+ def create_routine(
472
+ api_version: str,
473
+ name: str,
474
+ description: Optional[str],
475
+ workspace: Optional[str],
476
+ enabled: bool,
477
+ routine_type: Optional[str],
478
+ notebook_id: Optional[str],
479
+ trigger_json: Optional[str],
480
+ schedule_json: Optional[str],
481
+ event_json: Optional[str],
482
+ actions_json: Optional[str],
483
+ ) -> None:
484
+ """Create a new routine.
485
+
486
+ For v1 (notebook routines): --type and --notebook-id are required.
487
+ For v2 (event-action routines): --event and --actions are required.
488
+ """
489
+ try:
490
+ payload: Dict[str, Any] = {"name": name, "enabled": enabled}
491
+
492
+ if description is not None:
493
+ payload["description"] = description
494
+ if workspace is not None:
495
+ payload["workspace"] = workspace
496
+
497
+ if api_version == "v1":
498
+ if not routine_type:
499
+ click.echo("✗ --type is required for --api-version v1", err=True)
500
+ sys.exit(ExitCodes.INVALID_INPUT)
501
+ if not notebook_id:
502
+ click.echo("✗ --notebook-id is required for --api-version v1", err=True)
503
+ sys.exit(ExitCodes.INVALID_INPUT)
504
+
505
+ payload["type"] = routine_type
506
+ payload["execution"] = {
507
+ "type": "NOTEBOOK",
508
+ "definition": {"notebookId": notebook_id},
509
+ }
510
+
511
+ if routine_type == "TRIGGERED":
512
+ if not trigger_json:
513
+ click.echo("✗ --trigger is required for --type TRIGGERED", err=True)
514
+ sys.exit(ExitCodes.INVALID_INPUT)
515
+ payload["trigger"] = _parse_json_option(trigger_json, "trigger")
516
+ elif routine_type == "SCHEDULED":
517
+ if not schedule_json:
518
+ click.echo("✗ --schedule is required for --type SCHEDULED", err=True)
519
+ sys.exit(ExitCodes.INVALID_INPUT)
520
+ payload["schedule"] = _parse_json_option(schedule_json, "schedule")
521
+
522
+ else: # v2
523
+ if not event_json:
524
+ click.echo("✗ --event is required for --api-version v2", err=True)
525
+ sys.exit(ExitCodes.INVALID_INPUT)
526
+ if not actions_json:
527
+ click.echo("✗ --actions is required for --api-version v2", err=True)
528
+ sys.exit(ExitCodes.INVALID_INPUT)
529
+
530
+ payload["event"] = _parse_json_option(event_json, "event")
531
+ payload["actions"] = _parse_json_option(actions_json, "actions")
532
+
533
+ url = _routine_base_url(api_version)
534
+ resp = make_api_request("POST", url, payload=payload, handle_errors=False)
535
+ result = resp.json()
536
+
537
+ routine_id = result.get("id", "")
538
+ format_success("Routine created", {"id": routine_id, "name": name})
539
+
540
+ except SystemExit:
541
+ raise
542
+ except requests.exceptions.HTTPError as exc:
543
+ # Try to surface the API's own error message from the response body
544
+ api_msg: Optional[str] = None
545
+ try:
546
+ body = exc.response.json()
547
+ api_msg = body.get("error", {}).get("message")
548
+ except Exception:
549
+ pass
550
+ if api_msg:
551
+ click.echo(f"✗ Error: {api_msg}", err=True)
552
+ status_code = exc.response.status_code if exc.response is not None else 0
553
+ sys.exit(ExitCodes.INVALID_INPUT if status_code == 400 else ExitCodes.GENERAL_ERROR)
554
+ handle_api_error(exc)
555
+ except Exception as exc:
556
+ handle_api_error(exc)
557
+
558
+ # ------------------------------------------------------------------
559
+ # update
560
+ # ------------------------------------------------------------------
561
+
562
+ @routine.command(name="update")
563
+ @click.argument("routine_id")
564
+ @click.option(
565
+ "--api-version",
566
+ type=click.Choice(["v1", "v2"]),
567
+ default="v2",
568
+ show_default=True,
569
+ help="API version to use",
570
+ )
571
+ @click.option("--name", "-n", type=str, default=None, help="New name for the routine")
572
+ @click.option("--description", "-d", type=str, default=None, help="New description")
573
+ @click.option("--workspace", "-w", type=str, default=None, help="New workspace ID")
574
+ @click.option(
575
+ "--enable/--disable",
576
+ "enabled",
577
+ default=None,
578
+ help="Enable or disable the routine",
579
+ )
580
+ # v1-specific options
581
+ @click.option(
582
+ "--notebook-id",
583
+ type=str,
584
+ default=None,
585
+ help="New notebook ID to execute (v1 only)",
586
+ )
587
+ @click.option(
588
+ "--trigger",
589
+ "trigger_json",
590
+ type=str,
591
+ default=None,
592
+ help="Updated trigger definition as JSON string (v1 only)",
593
+ )
594
+ @click.option(
595
+ "--schedule",
596
+ "schedule_json",
597
+ type=str,
598
+ default=None,
599
+ help="Updated schedule definition as JSON string (v1 only)",
600
+ )
601
+ # v2-specific options
602
+ @click.option(
603
+ "--event",
604
+ "event_json",
605
+ type=str,
606
+ default=None,
607
+ help="Updated event definition as JSON string (v2 only)",
608
+ )
609
+ @click.option(
610
+ "--actions",
611
+ "actions_json",
612
+ type=str,
613
+ default=None,
614
+ help="Updated actions as a JSON array string (v2 only)",
615
+ )
616
+ def update_routine(
617
+ routine_id: str,
618
+ api_version: str,
619
+ name: Optional[str],
620
+ description: Optional[str],
621
+ workspace: Optional[str],
622
+ enabled: Optional[bool],
623
+ notebook_id: Optional[str],
624
+ trigger_json: Optional[str],
625
+ schedule_json: Optional[str],
626
+ event_json: Optional[str],
627
+ actions_json: Optional[str],
628
+ ) -> None:
629
+ """Update a routine by ID.
630
+
631
+ ROUTINE_ID is the unique identifier of the routine to update.
632
+ Only the provided fields will be updated.
633
+ """
634
+ try:
635
+ payload: Dict[str, Any] = {}
636
+
637
+ if name is not None:
638
+ payload["name"] = name
639
+ if description is not None:
640
+ payload["description"] = description
641
+ if workspace is not None:
642
+ payload["workspace"] = workspace
643
+ if enabled is not None:
644
+ payload["enabled"] = enabled
645
+
646
+ if api_version == "v1":
647
+ if notebook_id is not None:
648
+ payload["execution"] = {
649
+ "type": "NOTEBOOK",
650
+ "definition": {"notebookId": notebook_id},
651
+ }
652
+ if trigger_json is not None:
653
+ payload["trigger"] = _parse_json_option(trigger_json, "trigger")
654
+ if schedule_json is not None:
655
+ payload["schedule"] = _parse_json_option(schedule_json, "schedule")
656
+ else: # v2
657
+ if event_json is not None:
658
+ payload["event"] = _parse_json_option(event_json, "event")
659
+ if actions_json is not None:
660
+ payload["actions"] = _parse_json_option(actions_json, "actions")
661
+
662
+ if not payload:
663
+ click.echo("✗ No update fields provided. Specify at least one option.", err=True)
664
+ sys.exit(ExitCodes.INVALID_INPUT)
665
+
666
+ url = f"{_routine_base_url(api_version)}/{routine_id}"
667
+ make_api_request("PATCH", url, payload=payload)
668
+
669
+ format_success("Routine updated", {"id": routine_id})
670
+
671
+ except SystemExit:
672
+ raise
673
+ except Exception as exc:
674
+ handle_api_error(exc)
675
+
676
+ # ------------------------------------------------------------------
677
+ # enable
678
+ # ------------------------------------------------------------------
679
+
680
+ @routine.command(name="enable")
681
+ @click.argument("routine_id")
682
+ @click.option(
683
+ "--api-version",
684
+ type=click.Choice(["v1", "v2"]),
685
+ default="v2",
686
+ show_default=True,
687
+ help="API version to use",
688
+ )
689
+ def enable_routine(routine_id: str, api_version: str) -> None:
690
+ """Enable a routine by ID.
691
+
692
+ ROUTINE_ID is the unique identifier of the routine to enable.
693
+ """
694
+ try:
695
+ url = f"{_routine_base_url(api_version)}/{routine_id}"
696
+ make_api_request("PATCH", url, payload={"enabled": True})
697
+ format_success("Routine enabled", {"id": routine_id})
698
+ except Exception as exc:
699
+ handle_api_error(exc)
700
+
701
+ # ------------------------------------------------------------------
702
+ # disable
703
+ # ------------------------------------------------------------------
704
+
705
+ @routine.command(name="disable")
706
+ @click.argument("routine_id")
707
+ @click.option(
708
+ "--api-version",
709
+ type=click.Choice(["v1", "v2"]),
710
+ default="v2",
711
+ show_default=True,
712
+ help="API version to use",
713
+ )
714
+ def disable_routine(routine_id: str, api_version: str) -> None:
715
+ """Disable a routine by ID.
716
+
717
+ ROUTINE_ID is the unique identifier of the routine to disable.
718
+ """
719
+ try:
720
+ url = f"{_routine_base_url(api_version)}/{routine_id}"
721
+ make_api_request("PATCH", url, payload={"enabled": False})
722
+ format_success("Routine disabled", {"id": routine_id})
723
+ except Exception as exc:
724
+ handle_api_error(exc)
725
+
726
+ # ------------------------------------------------------------------
727
+ # delete
728
+ # ------------------------------------------------------------------
729
+
730
+ @routine.command(name="delete")
731
+ @click.argument("routine_id")
732
+ @click.option(
733
+ "--api-version",
734
+ type=click.Choice(["v1", "v2"]),
735
+ default="v2",
736
+ show_default=True,
737
+ help="API version to use",
738
+ )
739
+ @click.option(
740
+ "--yes",
741
+ "-y",
742
+ is_flag=True,
743
+ default=False,
744
+ help="Skip confirmation prompt",
745
+ )
746
+ def delete_routine(routine_id: str, api_version: str, yes: bool) -> None:
747
+ """Delete a routine by ID.
748
+
749
+ ROUTINE_ID is the unique identifier of the routine to delete.
750
+ """
751
+ if not yes:
752
+ if not questionary.confirm(
753
+ f"Are you sure you want to delete routine '{routine_id}'?",
754
+ default=False,
755
+ ).ask():
756
+ raise click.Abort()
757
+
758
+ try:
759
+ url = f"{_routine_base_url(api_version)}/{routine_id}"
760
+ make_api_request("DELETE", url, payload=None)
761
+ format_success("Routine deleted", {"id": routine_id})
762
+ except Exception as exc:
763
+ handle_api_error(exc)