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.
- slcli/__init__.py +1 -0
- slcli/__main__.py +23 -0
- slcli/_version.py +4 -0
- slcli/asset_click.py +1289 -0
- slcli/cli_formatters.py +218 -0
- slcli/cli_utils.py +504 -0
- slcli/comment_click.py +602 -0
- slcli/completion_click.py +418 -0
- slcli/config.py +81 -0
- slcli/config_click.py +498 -0
- slcli/dff_click.py +979 -0
- slcli/dff_decorators.py +24 -0
- slcli/example_click.py +404 -0
- slcli/example_loader.py +274 -0
- slcli/example_provisioner.py +2777 -0
- slcli/examples/README.md +134 -0
- slcli/examples/_schema/schema-v1.0.json +169 -0
- slcli/examples/demo-complete-workflow/README.md +323 -0
- slcli/examples/demo-complete-workflow/config.yaml +638 -0
- slcli/examples/demo-test-plans/README.md +132 -0
- slcli/examples/demo-test-plans/config.yaml +154 -0
- slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
- slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
- slcli/examples/exercise-7-1-test-plans/README.md +93 -0
- slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
- slcli/examples/spec-compliance-notebooks/README.md +140 -0
- slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
- slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- slcli/feed_click.py +892 -0
- slcli/file_click.py +932 -0
- slcli/function_click.py +1400 -0
- slcli/function_templates.py +85 -0
- slcli/main.py +406 -0
- slcli/mcp_click.py +269 -0
- slcli/mcp_server.py +748 -0
- slcli/notebook_click.py +1770 -0
- slcli/platform.py +345 -0
- slcli/policy_click.py +679 -0
- slcli/policy_utils.py +411 -0
- slcli/profiles.py +411 -0
- slcli/response_handlers.py +359 -0
- slcli/routine_click.py +763 -0
- slcli/skill_click.py +253 -0
- slcli/skills/slcli/SKILL.md +713 -0
- slcli/skills/slcli/references/analysis-recipes.md +474 -0
- slcli/skills/slcli/references/filtering.md +236 -0
- slcli/skills/systemlink-webapp/SKILL.md +744 -0
- slcli/skills/systemlink-webapp/references/deployment.md +123 -0
- slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
- slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
- slcli/ssl_trust.py +93 -0
- slcli/system_click.py +2216 -0
- slcli/table_utils.py +124 -0
- slcli/tag_click.py +794 -0
- slcli/templates_click.py +599 -0
- slcli/testmonitor_click.py +1667 -0
- slcli/universal_handlers.py +305 -0
- slcli/user_click.py +1218 -0
- slcli/utils.py +832 -0
- slcli/web_editor.py +295 -0
- slcli/webapp_click.py +981 -0
- slcli/workflow_preview.py +287 -0
- slcli/workflows_click.py +988 -0
- slcli/workitem_click.py +2258 -0
- slcli/workspace_click.py +576 -0
- slcli/workspace_utils.py +206 -0
- systemlink_cli-1.3.1.dist-info/METADATA +20 -0
- systemlink_cli-1.3.1.dist-info/RECORD +74 -0
- systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
- systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
- 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)
|