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/feed_click.py
ADDED
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
"""CLI commands for managing NI Package Manager feeds.
|
|
2
|
+
|
|
3
|
+
This module provides commands for managing package feeds in SystemLink,
|
|
4
|
+
supporting both SLE (/nifeed/v1) and SLS (/nirepo/v1) APIs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
import questionary
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from .cli_utils import validate_output_format
|
|
18
|
+
from .platform import PLATFORM_SLS, get_platform
|
|
19
|
+
from .universal_handlers import FilteredResponse, UniversalResponseHandler
|
|
20
|
+
from .utils import (
|
|
21
|
+
ExitCodes,
|
|
22
|
+
format_success,
|
|
23
|
+
get_base_url,
|
|
24
|
+
get_workspace_id_with_fallback,
|
|
25
|
+
handle_api_error,
|
|
26
|
+
make_api_request,
|
|
27
|
+
)
|
|
28
|
+
from .workspace_utils import get_effective_workspace, get_workspace_display_name, get_workspace_map
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class JobPollingError(Exception):
|
|
32
|
+
"""Raised when a job fails or cannot be retrieved."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JobNotFoundError(Exception):
|
|
36
|
+
"""Raised when a job is not found."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PackageUploadError(Exception):
|
|
40
|
+
"""Raised when a package upload response is missing required identifiers."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _get_feed_base_url() -> str:
|
|
44
|
+
"""Get the base URL for feed API.
|
|
45
|
+
|
|
46
|
+
Returns platform-specific URL:
|
|
47
|
+
- SLS (SystemLink Server): /nirepo/v1
|
|
48
|
+
- SLE (SystemLink Enterprise): /nifeed/v1
|
|
49
|
+
"""
|
|
50
|
+
if get_platform() == PLATFORM_SLS:
|
|
51
|
+
return f"{get_base_url()}/nirepo/v1"
|
|
52
|
+
return f"{get_base_url()}/nifeed/v1"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _normalize_platform(platform: str) -> str:
|
|
56
|
+
"""Normalize platform value based on current SystemLink platform.
|
|
57
|
+
|
|
58
|
+
SLE uses uppercase (WINDOWS, NI_LINUX_RT), SLS uses lowercase (windows, ni-linux-rt).
|
|
59
|
+
CLI accepts either case and normalizes appropriately.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
platform: Platform string (case-insensitive)
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Normalized platform string for the current API
|
|
66
|
+
"""
|
|
67
|
+
platform_lower = platform.lower().replace("_", "-")
|
|
68
|
+
is_sls = get_platform() == PLATFORM_SLS
|
|
69
|
+
|
|
70
|
+
if platform_lower in ("windows", "win"):
|
|
71
|
+
return "windows" if is_sls else "WINDOWS"
|
|
72
|
+
elif platform_lower in ("ni-linux-rt", "ni_linux_rt", "linux-rt", "linux_rt", "linuxrt"):
|
|
73
|
+
return "ni-linux-rt" if is_sls else "NI_LINUX_RT"
|
|
74
|
+
else:
|
|
75
|
+
# Return as-is for unknown platforms
|
|
76
|
+
return platform.lower() if is_sls else platform.upper()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _get_feed_name_field() -> str:
|
|
80
|
+
"""Get the feed name field based on platform.
|
|
81
|
+
|
|
82
|
+
SLS uses 'feedName', SLE uses 'name'.
|
|
83
|
+
"""
|
|
84
|
+
return "feedName" if get_platform() == PLATFORM_SLS else "name"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _extract_feed_name(feed: Dict[str, Any]) -> str:
|
|
88
|
+
"""Extract feed name from response, handling both SLE and SLS formats."""
|
|
89
|
+
return feed.get("name") or feed.get("feedName") or "Unknown"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _wait_for_job(
|
|
93
|
+
job_id: str, timeout: int = 300, poll_interval: int = 2, feed_id: Optional[str] = None
|
|
94
|
+
) -> Dict[str, Any]:
|
|
95
|
+
"""Wait for an async job to complete.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
job_id: The job ID to wait for
|
|
99
|
+
timeout: Maximum time to wait in seconds
|
|
100
|
+
poll_interval: Time between status checks in seconds
|
|
101
|
+
feed_id: Optional feed ID for feed-specific jobs
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Final job status dictionary
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
TimeoutError: If job doesn't complete within timeout
|
|
108
|
+
Exception: If job fails
|
|
109
|
+
"""
|
|
110
|
+
base_url = _get_feed_base_url()
|
|
111
|
+
if feed_id:
|
|
112
|
+
url = f"{base_url}/feeds/{feed_id}/jobs/{job_id}"
|
|
113
|
+
else:
|
|
114
|
+
url = f"{base_url}/jobs/{job_id}"
|
|
115
|
+
start_time = time.time()
|
|
116
|
+
|
|
117
|
+
while True:
|
|
118
|
+
elapsed = time.time() - start_time
|
|
119
|
+
if elapsed > timeout:
|
|
120
|
+
raise TimeoutError(f"Job {job_id} did not complete within {timeout} seconds")
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
resp = make_api_request("GET", url, handle_errors=False)
|
|
124
|
+
data = resp.json()
|
|
125
|
+
|
|
126
|
+
# Handle both response formats: {job: {...}} or direct {...}
|
|
127
|
+
job = data.get("job", data)
|
|
128
|
+
status = job.get("status", "").upper()
|
|
129
|
+
|
|
130
|
+
if status in ("SUCCESS", "SUCCEEDED", "COMPLETED"):
|
|
131
|
+
return job
|
|
132
|
+
if status in ("FAILED", "ERROR"):
|
|
133
|
+
error = job.get("error", {})
|
|
134
|
+
error_msg = error.get("message", "Unknown error")
|
|
135
|
+
raise JobPollingError(f"Job failed: {error_msg}")
|
|
136
|
+
if status in ("COMPLETED_WITH_ERROR",):
|
|
137
|
+
# Partial success - return but with warning
|
|
138
|
+
click.echo("⚠️ Job completed with errors", err=True)
|
|
139
|
+
return job
|
|
140
|
+
|
|
141
|
+
# Still processing - wait and retry
|
|
142
|
+
time.sleep(poll_interval)
|
|
143
|
+
|
|
144
|
+
except requests.exceptions.HTTPError as exc:
|
|
145
|
+
if exc.response is not None and exc.response.status_code == 404:
|
|
146
|
+
raise JobNotFoundError(f"Job {job_id} not found")
|
|
147
|
+
raise
|
|
148
|
+
except requests.RequestException:
|
|
149
|
+
raise
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _list_feeds(
|
|
153
|
+
platform: Optional[str] = None, workspace_id: Optional[str] = None
|
|
154
|
+
) -> List[Dict[str, Any]]:
|
|
155
|
+
"""List all feeds, optionally filtered by platform and workspace.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
platform: Optional platform filter (windows or ni-linux-rt)
|
|
159
|
+
workspace_id: Optional workspace ID filter
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
List of feed dictionaries
|
|
163
|
+
"""
|
|
164
|
+
base_url = _get_feed_base_url()
|
|
165
|
+
|
|
166
|
+
# Build query parameters
|
|
167
|
+
params = []
|
|
168
|
+
if platform:
|
|
169
|
+
normalized_platform = _normalize_platform(platform)
|
|
170
|
+
params.append(f"platform={normalized_platform}")
|
|
171
|
+
if workspace_id:
|
|
172
|
+
params.append(f"workspace={workspace_id}")
|
|
173
|
+
|
|
174
|
+
url = f"{base_url}/feeds"
|
|
175
|
+
if params:
|
|
176
|
+
url += "?" + "&".join(params)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
resp = make_api_request("GET", url)
|
|
180
|
+
data = resp.json()
|
|
181
|
+
return data.get("feeds", [])
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
handle_api_error(exc)
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _get_feed(feed_id: str) -> Dict[str, Any]:
|
|
188
|
+
"""Get a single feed by ID.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
feed_id: Feed ID
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Feed dictionary
|
|
195
|
+
"""
|
|
196
|
+
base_url = _get_feed_base_url()
|
|
197
|
+
url = f"{base_url}/feeds/{feed_id}"
|
|
198
|
+
|
|
199
|
+
resp = make_api_request("GET", url)
|
|
200
|
+
data = resp.json()
|
|
201
|
+
# Handle both response formats: {feed: {...}} or direct {...}
|
|
202
|
+
return data.get("feed", data)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _create_feed(
|
|
206
|
+
name: str, platform: str, description: Optional[str] = None, workspace: Optional[str] = None
|
|
207
|
+
) -> Dict[str, Any]:
|
|
208
|
+
"""Create a new feed.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
name: Feed name
|
|
212
|
+
platform: Target platform (windows or ni-linux-rt)
|
|
213
|
+
description: Optional feed description
|
|
214
|
+
workspace: Optional workspace ID
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Response dictionary containing job ID or feed details
|
|
218
|
+
"""
|
|
219
|
+
base_url = _get_feed_base_url()
|
|
220
|
+
url = f"{base_url}/feeds"
|
|
221
|
+
|
|
222
|
+
# Build payload with platform-specific field names
|
|
223
|
+
name_field = _get_feed_name_field()
|
|
224
|
+
payload: Dict[str, Any] = {
|
|
225
|
+
name_field: name,
|
|
226
|
+
"platform": _normalize_platform(platform),
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if description:
|
|
230
|
+
payload["description"] = description
|
|
231
|
+
if workspace:
|
|
232
|
+
payload["workspace"] = workspace
|
|
233
|
+
|
|
234
|
+
resp = make_api_request("POST", url, payload=payload)
|
|
235
|
+
return resp.json()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _delete_feed(feed_id: str) -> str:
|
|
239
|
+
"""Delete a feed.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
feed_id: Feed ID to delete
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Job ID for the async operation
|
|
246
|
+
"""
|
|
247
|
+
base_url = _get_feed_base_url()
|
|
248
|
+
url = f"{base_url}/feeds/{feed_id}"
|
|
249
|
+
|
|
250
|
+
resp = make_api_request("DELETE", url)
|
|
251
|
+
|
|
252
|
+
if resp.status_code == 204 or not resp.content:
|
|
253
|
+
return ""
|
|
254
|
+
|
|
255
|
+
data = resp.json()
|
|
256
|
+
return data.get("jobId", data.get("job", {}).get("id", ""))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _replicate_feed(
|
|
260
|
+
name: str,
|
|
261
|
+
platform: str,
|
|
262
|
+
source_url: str,
|
|
263
|
+
description: Optional[str] = None,
|
|
264
|
+
workspace: Optional[str] = None,
|
|
265
|
+
) -> Dict[str, Any]:
|
|
266
|
+
"""Replicate a feed from an external source.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
name: Name for the new feed
|
|
270
|
+
platform: Target platform
|
|
271
|
+
source_url: URL of the source feed to replicate
|
|
272
|
+
description: Optional description
|
|
273
|
+
workspace: Optional workspace ID
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Response dictionary containing job ID or feed details
|
|
277
|
+
"""
|
|
278
|
+
base_url = _get_feed_base_url()
|
|
279
|
+
url = f"{base_url}/replicate-feed"
|
|
280
|
+
|
|
281
|
+
# Build payload
|
|
282
|
+
name_field = _get_feed_name_field()
|
|
283
|
+
payload: Dict[str, Any] = {
|
|
284
|
+
name_field: name,
|
|
285
|
+
"platform": _normalize_platform(platform),
|
|
286
|
+
"urls": [source_url],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if description:
|
|
290
|
+
payload["description"] = description
|
|
291
|
+
if workspace:
|
|
292
|
+
payload["workspace"] = workspace
|
|
293
|
+
|
|
294
|
+
resp = make_api_request("POST", url, payload=payload)
|
|
295
|
+
return resp.json()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _list_packages(feed_id: str) -> List[Dict[str, Any]]:
|
|
299
|
+
"""List all packages in a feed.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
feed_id: Feed ID
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
List of package dictionaries
|
|
306
|
+
"""
|
|
307
|
+
base_url = _get_feed_base_url()
|
|
308
|
+
url = f"{base_url}/feeds/{feed_id}/packages"
|
|
309
|
+
|
|
310
|
+
resp = make_api_request("GET", url)
|
|
311
|
+
data = resp.json()
|
|
312
|
+
return data.get("packages", [])
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _upload_package_sle(feed_id: str, file_path: str, overwrite: bool = False) -> Dict[str, Any]:
|
|
316
|
+
"""Upload a package to a feed on SLE.
|
|
317
|
+
|
|
318
|
+
SLE uploads directly to the feed.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
feed_id: Feed ID
|
|
322
|
+
file_path: Path to the package file
|
|
323
|
+
overwrite: Whether to overwrite existing package
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Response dictionary containing job ID or package details
|
|
327
|
+
"""
|
|
328
|
+
base_url = _get_feed_base_url()
|
|
329
|
+
url = f"{base_url}/feeds/{feed_id}/packages"
|
|
330
|
+
if overwrite:
|
|
331
|
+
url += "?shouldOverwrite=true"
|
|
332
|
+
|
|
333
|
+
file_name = Path(file_path).name
|
|
334
|
+
with open(file_path, "rb") as f:
|
|
335
|
+
files = {"package": (file_name, f, "application/octet-stream")}
|
|
336
|
+
resp = make_api_request("POST", url, files=files)
|
|
337
|
+
|
|
338
|
+
return resp.json()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _upload_package_sls(feed_id: str, file_path: str, overwrite: bool = False) -> Dict[str, Any]:
|
|
342
|
+
"""Upload a package on SLS.
|
|
343
|
+
|
|
344
|
+
SLS uploads to the package pool first, then adds reference to feed.
|
|
345
|
+
|
|
346
|
+
Note: Even when the CLI command is invoked without ``--wait``, this helper must block on the
|
|
347
|
+
initial upload job to obtain the package ID before the feed association step can be queued.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
feed_id: Feed ID
|
|
351
|
+
file_path: Path to the package file
|
|
352
|
+
overwrite: Whether to overwrite existing package
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Response dictionary containing job ID and package ID
|
|
356
|
+
"""
|
|
357
|
+
base_url = _get_feed_base_url()
|
|
358
|
+
|
|
359
|
+
# Step 1: Upload to package pool
|
|
360
|
+
upload_url = f"{base_url}/upload-packages"
|
|
361
|
+
if overwrite:
|
|
362
|
+
upload_url += "?shouldOverwrite=true"
|
|
363
|
+
|
|
364
|
+
file_name = Path(file_path).name
|
|
365
|
+
with open(file_path, "rb") as f:
|
|
366
|
+
files = {"package": (file_name, f, "application/octet-stream")}
|
|
367
|
+
resp = make_api_request("POST", upload_url, files=files)
|
|
368
|
+
|
|
369
|
+
data = resp.json()
|
|
370
|
+
job_ids = data.get("jobIds", [])
|
|
371
|
+
|
|
372
|
+
if not job_ids:
|
|
373
|
+
raise PackageUploadError("No job ID returned from package upload")
|
|
374
|
+
|
|
375
|
+
# Wait for upload to complete to get package ID
|
|
376
|
+
job = _wait_for_job(job_ids[0], timeout=300)
|
|
377
|
+
package_id = job.get("resourceId")
|
|
378
|
+
|
|
379
|
+
if not package_id:
|
|
380
|
+
raise PackageUploadError("Package upload completed but no package ID returned")
|
|
381
|
+
|
|
382
|
+
# Step 2: Add package reference to feed
|
|
383
|
+
ref_url = f"{base_url}/feeds/{feed_id}/add-package-references"
|
|
384
|
+
ref_payload = {"packageReferences": [package_id]}
|
|
385
|
+
resp = make_api_request("POST", ref_url, payload=ref_payload)
|
|
386
|
+
|
|
387
|
+
data = resp.json()
|
|
388
|
+
# Inject packageId into response for CLI usage
|
|
389
|
+
result = data.copy()
|
|
390
|
+
result["packageId"] = package_id
|
|
391
|
+
return result
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _upload_package(feed_id: str, file_path: str, overwrite: bool = False) -> Dict[str, Any]:
|
|
395
|
+
"""Upload a package to a feed.
|
|
396
|
+
|
|
397
|
+
Routes to platform-specific implementation.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
feed_id: Feed ID
|
|
401
|
+
file_path: Path to the package file
|
|
402
|
+
overwrite: Whether to overwrite existing package
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Response dictionary containing job ID or package details
|
|
406
|
+
"""
|
|
407
|
+
if get_platform() == PLATFORM_SLS:
|
|
408
|
+
return _upload_package_sls(feed_id, file_path, overwrite)
|
|
409
|
+
return _upload_package_sle(feed_id, file_path, overwrite)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _delete_package(package_id: str) -> str:
|
|
413
|
+
"""Delete a package.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
package_id: Package ID to delete
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Job ID for the async operation
|
|
420
|
+
"""
|
|
421
|
+
base_url = _get_feed_base_url()
|
|
422
|
+
url = f"{base_url}/delete-packages"
|
|
423
|
+
payload = {"packageIds": [package_id]}
|
|
424
|
+
|
|
425
|
+
resp = make_api_request("POST", url, payload=payload)
|
|
426
|
+
|
|
427
|
+
if resp.status_code == 204 or not resp.content:
|
|
428
|
+
return ""
|
|
429
|
+
|
|
430
|
+
data = resp.json()
|
|
431
|
+
return data.get("jobId", data.get("job", {}).get("id", ""))
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def register_feed_commands(cli: Any) -> None:
|
|
435
|
+
"""Register the 'feed' command group and its subcommands."""
|
|
436
|
+
|
|
437
|
+
@cli.group()
|
|
438
|
+
def feed() -> None:
|
|
439
|
+
"""Manage NI Package Manager feeds and their packages.
|
|
440
|
+
|
|
441
|
+
Feeds are package repositories used by NI Package Manager to install
|
|
442
|
+
software on test systems. Supports Windows (.nipkg) and NI Linux RT
|
|
443
|
+
(.ipk/.deb) platforms.
|
|
444
|
+
"""
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
# -------------------------------------------------------------------------
|
|
448
|
+
# Feed management commands
|
|
449
|
+
# -------------------------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
@feed.command(name="list")
|
|
452
|
+
@click.option(
|
|
453
|
+
"--format",
|
|
454
|
+
"-f",
|
|
455
|
+
"format_",
|
|
456
|
+
type=click.Choice(["table", "json"]),
|
|
457
|
+
default="table",
|
|
458
|
+
help="Output format",
|
|
459
|
+
)
|
|
460
|
+
@click.option("--take", "-t", type=int, default=25, show_default=True, help="Items per page")
|
|
461
|
+
@click.option(
|
|
462
|
+
"--platform",
|
|
463
|
+
"-p",
|
|
464
|
+
type=click.Choice(["windows", "ni-linux-rt"], case_sensitive=False),
|
|
465
|
+
help="Filter by platform",
|
|
466
|
+
)
|
|
467
|
+
@click.option("--workspace", "-w", help="Filter by workspace name or ID")
|
|
468
|
+
def list_feeds(
|
|
469
|
+
format_: str, take: int, platform: Optional[str], workspace: Optional[str]
|
|
470
|
+
) -> None:
|
|
471
|
+
"""List feeds."""
|
|
472
|
+
format_output = validate_output_format(format_)
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
workspace_id = None
|
|
476
|
+
workspace = get_effective_workspace(workspace)
|
|
477
|
+
if workspace:
|
|
478
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
479
|
+
|
|
480
|
+
feeds = _list_feeds(platform=platform, workspace_id=workspace_id)
|
|
481
|
+
workspace_map = get_workspace_map()
|
|
482
|
+
|
|
483
|
+
def feed_formatter(f: Dict[str, Any]) -> List[str]:
|
|
484
|
+
ws_name = get_workspace_display_name(f.get("workspace", ""), workspace_map)
|
|
485
|
+
return [
|
|
486
|
+
_extract_feed_name(f),
|
|
487
|
+
f.get("platform", "").lower(),
|
|
488
|
+
f.get("id", ""),
|
|
489
|
+
ws_name,
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
mock_resp: Any = FilteredResponse({"feeds": feeds})
|
|
493
|
+
|
|
494
|
+
UniversalResponseHandler.handle_list_response(
|
|
495
|
+
resp=mock_resp,
|
|
496
|
+
data_key="feeds",
|
|
497
|
+
item_name="feed",
|
|
498
|
+
format_output=format_output,
|
|
499
|
+
formatter_func=feed_formatter,
|
|
500
|
+
headers=["Name", "Platform", "ID", "Workspace"],
|
|
501
|
+
column_widths=[30, 12, 36, 20],
|
|
502
|
+
empty_message="No feeds found.",
|
|
503
|
+
enable_pagination=True,
|
|
504
|
+
page_size=take,
|
|
505
|
+
)
|
|
506
|
+
except Exception as exc:
|
|
507
|
+
handle_api_error(exc)
|
|
508
|
+
|
|
509
|
+
@feed.command(name="get")
|
|
510
|
+
@click.option("--id", "-i", "feed_id", required=True, help="Feed ID")
|
|
511
|
+
@click.option(
|
|
512
|
+
"--format",
|
|
513
|
+
"-f",
|
|
514
|
+
"format_",
|
|
515
|
+
type=click.Choice(["table", "json"]),
|
|
516
|
+
default="table",
|
|
517
|
+
help="Output format",
|
|
518
|
+
)
|
|
519
|
+
def get_feed(feed_id: str, format_: str) -> None:
|
|
520
|
+
"""Show details for a feed."""
|
|
521
|
+
format_output = validate_output_format(format_)
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
feed_data = _get_feed(feed_id)
|
|
525
|
+
|
|
526
|
+
if format_output == "json":
|
|
527
|
+
click.echo(json.dumps(feed_data, indent=2))
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
workspace_map = get_workspace_map()
|
|
531
|
+
ws_name = get_workspace_display_name(feed_data.get("workspace", ""), workspace_map)
|
|
532
|
+
|
|
533
|
+
click.echo("Feed Details:")
|
|
534
|
+
click.echo("=" * 50)
|
|
535
|
+
click.echo(f"ID: {feed_data.get('id', 'N/A')}")
|
|
536
|
+
click.echo(f"Name: {_extract_feed_name(feed_data)}")
|
|
537
|
+
click.echo(f"Platform: {feed_data.get('platform', 'N/A')}")
|
|
538
|
+
click.echo(f"Workspace: {ws_name}")
|
|
539
|
+
click.echo(f"Description: {feed_data.get('description', 'N/A')}")
|
|
540
|
+
click.echo(f"Directory: {feed_data.get('directoryUri', 'N/A')}")
|
|
541
|
+
click.echo(f"Ready: {feed_data.get('ready', 'N/A')}")
|
|
542
|
+
click.echo(f"Updated: {feed_data.get('lastUpdated', 'N/A')}")
|
|
543
|
+
|
|
544
|
+
# Show package sources if available
|
|
545
|
+
sources = feed_data.get("packageSources", [])
|
|
546
|
+
if sources:
|
|
547
|
+
click.echo(f"\nPackage Sources ({len(sources)}):")
|
|
548
|
+
for src in sources[:5]: # Limit display
|
|
549
|
+
click.echo(f" - {src}")
|
|
550
|
+
if len(sources) > 5:
|
|
551
|
+
click.echo(f" ... and {len(sources) - 5} more")
|
|
552
|
+
|
|
553
|
+
except Exception as exc:
|
|
554
|
+
handle_api_error(exc)
|
|
555
|
+
|
|
556
|
+
@feed.command(name="create")
|
|
557
|
+
@click.option("--name", "-n", required=True, help="Feed name")
|
|
558
|
+
@click.option(
|
|
559
|
+
"--platform",
|
|
560
|
+
"-p",
|
|
561
|
+
required=True,
|
|
562
|
+
type=click.Choice(["windows", "ni-linux-rt"], case_sensitive=False),
|
|
563
|
+
help="Target platform",
|
|
564
|
+
)
|
|
565
|
+
@click.option("--description", "-d", help="Feed description")
|
|
566
|
+
@click.option("--workspace", "-w", help="Workspace name or ID")
|
|
567
|
+
@click.option("--wait", is_flag=True, help="Wait for operation to complete")
|
|
568
|
+
@click.option("--timeout", type=int, default=300, help="Timeout in seconds when using --wait")
|
|
569
|
+
def create_feed(
|
|
570
|
+
name: str,
|
|
571
|
+
platform: str,
|
|
572
|
+
description: Optional[str],
|
|
573
|
+
workspace: Optional[str],
|
|
574
|
+
wait: bool,
|
|
575
|
+
timeout: int,
|
|
576
|
+
) -> None:
|
|
577
|
+
"""Create a feed."""
|
|
578
|
+
from .utils import check_readonly_mode
|
|
579
|
+
|
|
580
|
+
check_readonly_mode("create a feed")
|
|
581
|
+
|
|
582
|
+
try:
|
|
583
|
+
workspace_id = None
|
|
584
|
+
workspace = get_effective_workspace(workspace)
|
|
585
|
+
if workspace:
|
|
586
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
587
|
+
|
|
588
|
+
result = _create_feed(
|
|
589
|
+
name=name,
|
|
590
|
+
platform=platform,
|
|
591
|
+
description=description,
|
|
592
|
+
workspace=workspace_id,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
job_id = result.get("jobId", result.get("job", {}).get("id"))
|
|
596
|
+
|
|
597
|
+
if wait:
|
|
598
|
+
if job_id:
|
|
599
|
+
click.echo(f"Creating feed '{name}'... (job: {job_id})")
|
|
600
|
+
job = _wait_for_job(job_id, timeout=timeout)
|
|
601
|
+
feed_id = job.get("resourceId", "")
|
|
602
|
+
else:
|
|
603
|
+
# Synchronous creation
|
|
604
|
+
feed_id = result.get("id", "")
|
|
605
|
+
|
|
606
|
+
format_success("Feed created", {"ID": feed_id, "Name": name})
|
|
607
|
+
else:
|
|
608
|
+
if job_id:
|
|
609
|
+
format_success("Feed creation started", {"Job ID": job_id, "Name": name})
|
|
610
|
+
else:
|
|
611
|
+
feed_id = result.get("id", "")
|
|
612
|
+
format_success("Feed created", {"ID": feed_id, "Name": name})
|
|
613
|
+
|
|
614
|
+
except TimeoutError as exc:
|
|
615
|
+
click.echo(f"✗ {exc}", err=True)
|
|
616
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
617
|
+
except Exception as exc:
|
|
618
|
+
handle_api_error(exc)
|
|
619
|
+
|
|
620
|
+
@feed.command(name="delete")
|
|
621
|
+
@click.option("--id", "-i", "feed_id", required=True, help="Feed ID to delete")
|
|
622
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
623
|
+
@click.option("--wait", is_flag=True, help="Wait for operation to complete")
|
|
624
|
+
@click.option("--timeout", type=int, default=300, help="Timeout in seconds when using --wait")
|
|
625
|
+
def delete_feed(feed_id: str, yes: bool, wait: bool, timeout: int) -> None:
|
|
626
|
+
"""Delete a feed and its packages."""
|
|
627
|
+
from .utils import check_readonly_mode
|
|
628
|
+
|
|
629
|
+
check_readonly_mode("delete a feed")
|
|
630
|
+
|
|
631
|
+
if not yes:
|
|
632
|
+
if not questionary.confirm(
|
|
633
|
+
f"Are you sure you want to delete feed {feed_id}?",
|
|
634
|
+
default=False,
|
|
635
|
+
).ask():
|
|
636
|
+
click.echo("Cancelled.")
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
try:
|
|
640
|
+
job_id = _delete_feed(feed_id)
|
|
641
|
+
|
|
642
|
+
if not job_id:
|
|
643
|
+
format_success("Feed deleted", {"ID": feed_id})
|
|
644
|
+
return
|
|
645
|
+
|
|
646
|
+
if wait:
|
|
647
|
+
click.echo(f"Deleting feed... (job: {job_id})")
|
|
648
|
+
_wait_for_job(job_id, timeout=timeout)
|
|
649
|
+
format_success("Feed deleted", {"ID": feed_id})
|
|
650
|
+
else:
|
|
651
|
+
format_success("Feed deletion started", {"Job ID": job_id, "Feed ID": feed_id})
|
|
652
|
+
|
|
653
|
+
except TimeoutError as exc:
|
|
654
|
+
click.echo(f"✗ {exc}", err=True)
|
|
655
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
656
|
+
except Exception as exc:
|
|
657
|
+
handle_api_error(exc)
|
|
658
|
+
|
|
659
|
+
@feed.command(name="replicate")
|
|
660
|
+
@click.option("--name", "-n", required=True, help="Name for the new feed")
|
|
661
|
+
@click.option(
|
|
662
|
+
"--platform",
|
|
663
|
+
"-p",
|
|
664
|
+
required=True,
|
|
665
|
+
type=click.Choice(["windows", "ni-linux-rt"], case_sensitive=False),
|
|
666
|
+
help="Target platform",
|
|
667
|
+
)
|
|
668
|
+
@click.option("--url", "-u", required=True, help="Source feed URL to replicate from")
|
|
669
|
+
@click.option("--description", "-d", help="Feed description")
|
|
670
|
+
@click.option("--workspace", "-w", help="Workspace name or ID")
|
|
671
|
+
@click.option("--wait", is_flag=True, help="Wait for operation to complete")
|
|
672
|
+
@click.option("--timeout", type=int, default=600, help="Timeout in seconds when using --wait")
|
|
673
|
+
def replicate_feed(
|
|
674
|
+
name: str,
|
|
675
|
+
platform: str,
|
|
676
|
+
url: str,
|
|
677
|
+
description: Optional[str],
|
|
678
|
+
workspace: Optional[str],
|
|
679
|
+
wait: bool,
|
|
680
|
+
timeout: int,
|
|
681
|
+
) -> None:
|
|
682
|
+
"""Replicate a feed from an external URL."""
|
|
683
|
+
try:
|
|
684
|
+
workspace_id = None
|
|
685
|
+
workspace = get_effective_workspace(workspace)
|
|
686
|
+
if workspace:
|
|
687
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
688
|
+
|
|
689
|
+
result = _replicate_feed(
|
|
690
|
+
name=name,
|
|
691
|
+
platform=platform,
|
|
692
|
+
source_url=url,
|
|
693
|
+
description=description,
|
|
694
|
+
workspace=workspace_id,
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
job_id = result.get("jobId", result.get("job", {}).get("id"))
|
|
698
|
+
|
|
699
|
+
if wait:
|
|
700
|
+
if job_id:
|
|
701
|
+
click.echo(f"Replicating feed from {url}... (job: {job_id})")
|
|
702
|
+
click.echo("This may take several minutes for large feeds.")
|
|
703
|
+
job = _wait_for_job(job_id, timeout=timeout)
|
|
704
|
+
feed_id = job.get("resourceId", "")
|
|
705
|
+
else:
|
|
706
|
+
feed_id = result.get("id", "")
|
|
707
|
+
|
|
708
|
+
format_success("Feed replicated", {"ID": feed_id, "Name": name})
|
|
709
|
+
else:
|
|
710
|
+
if job_id:
|
|
711
|
+
format_success(
|
|
712
|
+
"Feed replication started",
|
|
713
|
+
{"Job ID": job_id, "Name": name, "Source": url},
|
|
714
|
+
)
|
|
715
|
+
else:
|
|
716
|
+
feed_id = result.get("id", "")
|
|
717
|
+
format_success("Feed replicated", {"ID": feed_id, "Name": name})
|
|
718
|
+
|
|
719
|
+
except TimeoutError as exc:
|
|
720
|
+
click.echo(f"✗ {exc}", err=True)
|
|
721
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
722
|
+
except Exception as exc:
|
|
723
|
+
handle_api_error(exc)
|
|
724
|
+
|
|
725
|
+
# -------------------------------------------------------------------------
|
|
726
|
+
# Package subgroup
|
|
727
|
+
# -------------------------------------------------------------------------
|
|
728
|
+
|
|
729
|
+
@feed.group(name="package")
|
|
730
|
+
def package() -> None:
|
|
731
|
+
"""Manage packages in a feed."""
|
|
732
|
+
pass
|
|
733
|
+
|
|
734
|
+
@package.command(name="list")
|
|
735
|
+
@click.option("--feed-id", "-f", required=True, help="Feed ID")
|
|
736
|
+
@click.option(
|
|
737
|
+
"--format",
|
|
738
|
+
"format_",
|
|
739
|
+
type=click.Choice(["table", "json"]),
|
|
740
|
+
default="table",
|
|
741
|
+
help="Output format",
|
|
742
|
+
)
|
|
743
|
+
@click.option("--take", "-t", type=int, default=25, show_default=True, help="Items per page")
|
|
744
|
+
def list_packages(feed_id: str, format_: str, take: int) -> None:
|
|
745
|
+
"""List packages in a feed."""
|
|
746
|
+
format_output = validate_output_format(format_)
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
packages = _list_packages(feed_id)
|
|
750
|
+
|
|
751
|
+
def package_formatter(p: Dict[str, Any]) -> List[str]:
|
|
752
|
+
metadata = p.get("metadata", {})
|
|
753
|
+
return [
|
|
754
|
+
metadata.get("packageName", p.get("id", "")[:20]),
|
|
755
|
+
metadata.get("version", "N/A"),
|
|
756
|
+
metadata.get("architecture", "N/A"),
|
|
757
|
+
p.get("id", ""),
|
|
758
|
+
]
|
|
759
|
+
|
|
760
|
+
mock_resp: Any = FilteredResponse({"packages": packages})
|
|
761
|
+
|
|
762
|
+
UniversalResponseHandler.handle_list_response(
|
|
763
|
+
resp=mock_resp,
|
|
764
|
+
data_key="packages",
|
|
765
|
+
item_name="package",
|
|
766
|
+
format_output=format_output,
|
|
767
|
+
formatter_func=package_formatter,
|
|
768
|
+
headers=["Name", "Version", "Architecture", "ID"],
|
|
769
|
+
column_widths=[30, 20, 15, 36],
|
|
770
|
+
empty_message="No packages found in this feed.",
|
|
771
|
+
enable_pagination=True,
|
|
772
|
+
page_size=take,
|
|
773
|
+
)
|
|
774
|
+
except Exception as exc:
|
|
775
|
+
handle_api_error(exc)
|
|
776
|
+
|
|
777
|
+
@package.command(name="upload")
|
|
778
|
+
@click.option("--feed-id", "-f", required=True, help="Feed ID to upload to")
|
|
779
|
+
@click.option("--file", "-i", "file_path", required=True, help="Path to package file")
|
|
780
|
+
@click.option("--overwrite", is_flag=True, help="Overwrite existing package")
|
|
781
|
+
@click.option(
|
|
782
|
+
"--wait",
|
|
783
|
+
is_flag=True,
|
|
784
|
+
help=(
|
|
785
|
+
"Wait for the final association job to finish (SLS still waits for initial pool upload)."
|
|
786
|
+
),
|
|
787
|
+
)
|
|
788
|
+
@click.option("--timeout", type=int, default=300, help="Timeout in seconds when using --wait")
|
|
789
|
+
def upload_package(
|
|
790
|
+
feed_id: str, file_path: str, overwrite: bool, wait: bool, timeout: int
|
|
791
|
+
) -> None:
|
|
792
|
+
"""Upload a package to a feed."""
|
|
793
|
+
from .utils import check_readonly_mode
|
|
794
|
+
|
|
795
|
+
check_readonly_mode("upload a feed package")
|
|
796
|
+
|
|
797
|
+
# Validate file exists
|
|
798
|
+
path = Path(file_path)
|
|
799
|
+
if not path.exists():
|
|
800
|
+
click.echo(f"✗ File not found: {file_path}", err=True)
|
|
801
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
802
|
+
|
|
803
|
+
if not path.is_file():
|
|
804
|
+
click.echo(f"✗ Not a file: {file_path}", err=True)
|
|
805
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
806
|
+
|
|
807
|
+
try:
|
|
808
|
+
platform_value = get_platform()
|
|
809
|
+
if platform_value == PLATFORM_SLS and not wait:
|
|
810
|
+
click.echo(
|
|
811
|
+
"Note: SLS uploads wait for the initial package pool upload even without --wait.",
|
|
812
|
+
err=False,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
result = _upload_package(feed_id, file_path, overwrite)
|
|
816
|
+
job_id = result.get("jobId", result.get("job", {}).get("id", ""))
|
|
817
|
+
|
|
818
|
+
if wait:
|
|
819
|
+
if job_id:
|
|
820
|
+
click.echo(f"Uploading {path.name}... (job: {job_id})")
|
|
821
|
+
try:
|
|
822
|
+
job = _wait_for_job(job_id, timeout=timeout, feed_id=feed_id)
|
|
823
|
+
package_id = job.get("resourceId", "")
|
|
824
|
+
except JobNotFoundError:
|
|
825
|
+
# If job is gone but we have a package ID (e.g. SLS), assume success
|
|
826
|
+
pkg_id_from_start = result.get("packageId")
|
|
827
|
+
if pkg_id_from_start:
|
|
828
|
+
package_id = pkg_id_from_start
|
|
829
|
+
click.echo(
|
|
830
|
+
"⚠️ Job not found, but package ID is known. Assuming success.",
|
|
831
|
+
err=True,
|
|
832
|
+
)
|
|
833
|
+
else:
|
|
834
|
+
raise
|
|
835
|
+
else:
|
|
836
|
+
# Synchronous success
|
|
837
|
+
package_id = result.get("id", result.get("packageId", ""))
|
|
838
|
+
|
|
839
|
+
format_success("Package uploaded", {"ID": package_id, "File": path.name})
|
|
840
|
+
else:
|
|
841
|
+
if job_id:
|
|
842
|
+
format_success("Package upload started", {"Job ID": job_id, "File": path.name})
|
|
843
|
+
else:
|
|
844
|
+
package_id = result.get("id", result.get("packageId", ""))
|
|
845
|
+
format_success("Package uploaded", {"ID": package_id, "File": path.name})
|
|
846
|
+
|
|
847
|
+
except TimeoutError as exc:
|
|
848
|
+
click.echo(f"✗ {exc}", err=True)
|
|
849
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
850
|
+
except Exception as exc:
|
|
851
|
+
handle_api_error(exc)
|
|
852
|
+
|
|
853
|
+
@package.command(name="delete")
|
|
854
|
+
@click.option("--id", "-i", "package_id", required=True, help="Package ID to delete")
|
|
855
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
856
|
+
@click.option("--wait", is_flag=True, help="Wait for operation to complete")
|
|
857
|
+
@click.option("--timeout", type=int, default=300, help="Timeout in seconds when using --wait")
|
|
858
|
+
def delete_package(package_id: str, yes: bool, wait: bool, timeout: int) -> None:
|
|
859
|
+
"""Delete a package from a feed."""
|
|
860
|
+
from .utils import check_readonly_mode
|
|
861
|
+
|
|
862
|
+
check_readonly_mode("delete a package")
|
|
863
|
+
|
|
864
|
+
if not yes:
|
|
865
|
+
if not questionary.confirm(
|
|
866
|
+
f"Are you sure you want to delete package {package_id}?",
|
|
867
|
+
default=False,
|
|
868
|
+
).ask():
|
|
869
|
+
click.echo("Cancelled.")
|
|
870
|
+
return
|
|
871
|
+
|
|
872
|
+
try:
|
|
873
|
+
job_id = _delete_package(package_id)
|
|
874
|
+
|
|
875
|
+
if wait:
|
|
876
|
+
if job_id:
|
|
877
|
+
click.echo(f"Deleting package... (job: {job_id})")
|
|
878
|
+
_wait_for_job(job_id, timeout=timeout)
|
|
879
|
+
format_success("Package deleted", {"ID": package_id})
|
|
880
|
+
else:
|
|
881
|
+
if job_id:
|
|
882
|
+
format_success(
|
|
883
|
+
"Package deletion started", {"Job ID": job_id, "Package ID": package_id}
|
|
884
|
+
)
|
|
885
|
+
else:
|
|
886
|
+
format_success("Package deleted", {"ID": package_id})
|
|
887
|
+
|
|
888
|
+
except TimeoutError as exc:
|
|
889
|
+
click.echo(f"✗ {exc}", err=True)
|
|
890
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
891
|
+
except Exception as exc:
|
|
892
|
+
handle_api_error(exc)
|