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/function_click.py
ADDED
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
"""CLI commands for managing SystemLink WebAssembly function definitions and executions."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import questionary
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from .cli_utils import validate_output_format
|
|
14
|
+
from .function_templates import (
|
|
15
|
+
download_and_extract_template,
|
|
16
|
+
TEMPLATE_REPO,
|
|
17
|
+
TEMPLATE_BRANCH,
|
|
18
|
+
TEMPLATE_SUBFOLDERS,
|
|
19
|
+
)
|
|
20
|
+
from .platform import require_feature
|
|
21
|
+
from .universal_handlers import UniversalResponseHandler, FilteredResponse
|
|
22
|
+
from .utils import (
|
|
23
|
+
display_api_errors,
|
|
24
|
+
ExitCodes,
|
|
25
|
+
get_base_url,
|
|
26
|
+
get_headers,
|
|
27
|
+
get_ssl_verify,
|
|
28
|
+
get_workspace_id_with_fallback,
|
|
29
|
+
get_workspace_map,
|
|
30
|
+
handle_api_error,
|
|
31
|
+
load_json_file,
|
|
32
|
+
make_api_request,
|
|
33
|
+
)
|
|
34
|
+
from .workspace_utils import (
|
|
35
|
+
get_effective_workspace,
|
|
36
|
+
get_workspace_display_name,
|
|
37
|
+
resolve_workspace_filter,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_env_file() -> Dict[str, str]:
|
|
42
|
+
"""Load environment variables from a .env file in the current directory.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dictionary of environment variables from .env file
|
|
46
|
+
"""
|
|
47
|
+
env_vars = {}
|
|
48
|
+
env_file = Path.cwd() / ".env"
|
|
49
|
+
|
|
50
|
+
if env_file.exists():
|
|
51
|
+
try:
|
|
52
|
+
with open(env_file, "r", encoding="utf-8") as f:
|
|
53
|
+
for line in f:
|
|
54
|
+
line = line.strip()
|
|
55
|
+
if line and not line.startswith("#") and "=" in line:
|
|
56
|
+
key, value = line.split("=", 1)
|
|
57
|
+
env_vars[key.strip()] = value.strip().strip('"').strip("'")
|
|
58
|
+
except Exception:
|
|
59
|
+
# Silently ignore .env file parsing errors
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
return env_vars
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_function_service_base_url() -> str:
|
|
66
|
+
"""Get the unified base URL for Function Management Service (v2).
|
|
67
|
+
|
|
68
|
+
The unified service consolidates function definition and execution.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Base URL (prefix) for the unified Function Management Service (without version suffix)
|
|
72
|
+
"""
|
|
73
|
+
env_vars = load_env_file()
|
|
74
|
+
|
|
75
|
+
# Prefer explicit FUNCTION_SERVICE_URL
|
|
76
|
+
function_url = env_vars.get("FUNCTION_SERVICE_URL") or os.environ.get("FUNCTION_SERVICE_URL")
|
|
77
|
+
if function_url:
|
|
78
|
+
# Normalize to include /nifunction
|
|
79
|
+
return (
|
|
80
|
+
function_url if function_url.endswith("/nifunction") else f"{function_url}/nifunction"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Fallback to global SYSTEMLINK_API_URL (handled by get_base_url)
|
|
84
|
+
base_url = get_base_url()
|
|
85
|
+
return f"{base_url}/nifunction"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_unified_v2_base() -> str:
|
|
89
|
+
"""Get the versioned root for unified Function Management Service (v2)."""
|
|
90
|
+
return f"{get_function_service_base_url()}/v2"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _query_all_functions(
|
|
94
|
+
workspace_filter: Optional[str] = None,
|
|
95
|
+
name_filter: Optional[str] = None,
|
|
96
|
+
interface_filter: Optional[str] = None,
|
|
97
|
+
custom_filter: Optional[str] = None,
|
|
98
|
+
workspace_map: Optional[Dict[str, Any]] = None,
|
|
99
|
+
) -> List[Dict[str, Any]]:
|
|
100
|
+
"""Query all function definitions using continuation token pagination.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
workspace_filter: Optional workspace ID to filter by
|
|
104
|
+
name_filter: Optional name pattern to filter by
|
|
105
|
+
interface_filter: Optional text to search for in the interface property
|
|
106
|
+
custom_filter: Optional custom Dynamic LINQ filter expression
|
|
107
|
+
workspace_map: Optional workspace mapping to avoid repeated lookups
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of all function definitions matching the filters
|
|
111
|
+
"""
|
|
112
|
+
url = f"{get_unified_v2_base()}/query-functions"
|
|
113
|
+
all_functions = []
|
|
114
|
+
continuation_token = None
|
|
115
|
+
|
|
116
|
+
while True:
|
|
117
|
+
# Build payload for the request
|
|
118
|
+
payload: Dict[str, Union[int, str, List[str]]] = {
|
|
119
|
+
"take": 100, # Use smaller page size for efficient pagination
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Build filter expression
|
|
123
|
+
filter_parts = []
|
|
124
|
+
|
|
125
|
+
if workspace_filter:
|
|
126
|
+
filter_parts.append(f'workspaceId == "{workspace_filter}"')
|
|
127
|
+
|
|
128
|
+
if name_filter:
|
|
129
|
+
filter_parts.append(f'name.StartsWith("{name_filter}")')
|
|
130
|
+
|
|
131
|
+
if interface_filter:
|
|
132
|
+
filter_parts.append(f'interface.Contains("{interface_filter}")')
|
|
133
|
+
|
|
134
|
+
# Always filter for WASM runtime by checking for interface.entrypoint since CLI is WASM-only
|
|
135
|
+
# Functions with interface.entrypoint are WASM functions
|
|
136
|
+
filter_parts.append('interface.entrypoint != null && interface.entrypoint != ""')
|
|
137
|
+
|
|
138
|
+
# Add custom filter if provided (this will override automatic filters if both are used)
|
|
139
|
+
if custom_filter:
|
|
140
|
+
if filter_parts:
|
|
141
|
+
# Combine automatic filters with custom filter using AND
|
|
142
|
+
combined_filter = f'({" && ".join(filter_parts)}) && ({custom_filter})'
|
|
143
|
+
payload["filter"] = combined_filter
|
|
144
|
+
else:
|
|
145
|
+
payload["filter"] = custom_filter
|
|
146
|
+
elif filter_parts:
|
|
147
|
+
payload["filter"] = " && ".join(filter_parts)
|
|
148
|
+
|
|
149
|
+
# Add continuation token if we have one
|
|
150
|
+
if continuation_token:
|
|
151
|
+
payload["continuationToken"] = continuation_token
|
|
152
|
+
|
|
153
|
+
resp = make_api_request("POST", url, payload)
|
|
154
|
+
data = resp.json()
|
|
155
|
+
|
|
156
|
+
# Extract functions from this page
|
|
157
|
+
functions = data.get("functions", [])
|
|
158
|
+
all_functions.extend(functions)
|
|
159
|
+
|
|
160
|
+
# Check if there are more pages
|
|
161
|
+
continuation_token = data.get("continuationToken")
|
|
162
|
+
if not continuation_token:
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
return all_functions
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _query_all_executions(
|
|
169
|
+
workspace_filter: Optional[str] = None,
|
|
170
|
+
status_filter: Optional[str] = None,
|
|
171
|
+
function_id_filter: Optional[str] = None,
|
|
172
|
+
workspace_map: Optional[Dict[str, Any]] = None,
|
|
173
|
+
) -> List[Dict[str, Any]]:
|
|
174
|
+
"""Query all function executions using continuation token pagination.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
workspace_filter: Optional workspace ID to filter by
|
|
178
|
+
status_filter: Optional execution status to filter by
|
|
179
|
+
function_id_filter: Optional function ID to filter by
|
|
180
|
+
workspace_map: Optional workspace mapping to avoid repeated lookups
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
List of all function executions matching the filters
|
|
184
|
+
"""
|
|
185
|
+
url = f"{get_unified_v2_base()}/query-executions"
|
|
186
|
+
all_executions = []
|
|
187
|
+
continuation_token = None
|
|
188
|
+
|
|
189
|
+
while True:
|
|
190
|
+
# Build payload for the request
|
|
191
|
+
payload: Dict[str, Union[int, str, List[str]]] = {
|
|
192
|
+
"take": 100, # Use smaller page size for efficient pagination
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Build filter expression
|
|
196
|
+
filter_parts = []
|
|
197
|
+
|
|
198
|
+
if workspace_filter:
|
|
199
|
+
filter_parts.append(f'workspaceId == "{workspace_filter}"')
|
|
200
|
+
|
|
201
|
+
if status_filter:
|
|
202
|
+
filter_parts.append(f'status == "{status_filter}"')
|
|
203
|
+
|
|
204
|
+
if function_id_filter:
|
|
205
|
+
filter_parts.append(f'functionId == "{function_id_filter}"')
|
|
206
|
+
|
|
207
|
+
if filter_parts:
|
|
208
|
+
payload["filter"] = " && ".join(filter_parts)
|
|
209
|
+
|
|
210
|
+
# Add continuation token if we have one
|
|
211
|
+
if continuation_token:
|
|
212
|
+
payload["continuationToken"] = continuation_token
|
|
213
|
+
|
|
214
|
+
resp = make_api_request("POST", url, payload)
|
|
215
|
+
data = resp.json()
|
|
216
|
+
|
|
217
|
+
# Extract executions from this page
|
|
218
|
+
executions = data.get("executions", [])
|
|
219
|
+
all_executions.extend(executions)
|
|
220
|
+
|
|
221
|
+
# Check if there are more pages
|
|
222
|
+
continuation_token = data.get("continuationToken")
|
|
223
|
+
if not continuation_token:
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
return all_executions
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def register_function_commands(cli: Any) -> None:
|
|
230
|
+
"""Register the 'function' command group and its subcommands."""
|
|
231
|
+
|
|
232
|
+
@cli.group(hidden=True)
|
|
233
|
+
@click.pass_context
|
|
234
|
+
def function(ctx: click.Context) -> None:
|
|
235
|
+
"""Manage function definitions and executions."""
|
|
236
|
+
# Check for platform feature availability
|
|
237
|
+
# Only check if a subcommand is being invoked (not just --help)
|
|
238
|
+
if ctx.invoked_subcommand is not None:
|
|
239
|
+
require_feature("function_execution")
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
# Initialization (template bootstrap) command
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
@function.command(name="init")
|
|
246
|
+
@click.option(
|
|
247
|
+
"--language",
|
|
248
|
+
"-l",
|
|
249
|
+
type=click.Choice(["typescript", "python", "ts", "py"], case_sensitive=False),
|
|
250
|
+
help="Template language (typescript|python). Will prompt if omitted.",
|
|
251
|
+
)
|
|
252
|
+
@click.option(
|
|
253
|
+
"--directory",
|
|
254
|
+
"-d",
|
|
255
|
+
type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
|
|
256
|
+
help="Target directory to create or populate (defaults to current working directory)",
|
|
257
|
+
)
|
|
258
|
+
@click.option(
|
|
259
|
+
"--force",
|
|
260
|
+
is_flag=True,
|
|
261
|
+
help="Overwrite existing non-empty directory contents.",
|
|
262
|
+
)
|
|
263
|
+
def init_function_template(
|
|
264
|
+
language: Optional[str], directory: Optional[Path], force: bool
|
|
265
|
+
) -> None:
|
|
266
|
+
"""Initialize a local function template (TypeScript Hono or Python HTTP)."""
|
|
267
|
+
try:
|
|
268
|
+
# Prompt for language if not supplied
|
|
269
|
+
if not language:
|
|
270
|
+
language = questionary.select(
|
|
271
|
+
"Select language?",
|
|
272
|
+
choices=["typescript", "python"],
|
|
273
|
+
).ask()
|
|
274
|
+
if language is None:
|
|
275
|
+
raise click.Abort()
|
|
276
|
+
if not language:
|
|
277
|
+
click.echo("✗ Language not specified.", err=True)
|
|
278
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
279
|
+
language_norm = language.lower()
|
|
280
|
+
if language_norm in {"ts"}:
|
|
281
|
+
language_norm = "typescript"
|
|
282
|
+
if language_norm in {"py"}:
|
|
283
|
+
language_norm = "python"
|
|
284
|
+
if language_norm not in {"typescript", "python"}:
|
|
285
|
+
click.echo("✗ Unsupported language.", err=True)
|
|
286
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
287
|
+
|
|
288
|
+
# Prompt for directory if not supplied
|
|
289
|
+
if directory is None:
|
|
290
|
+
dir_input = click.prompt(
|
|
291
|
+
"Target directory (leave blank for current directory)",
|
|
292
|
+
default="",
|
|
293
|
+
show_default=False,
|
|
294
|
+
)
|
|
295
|
+
if dir_input.strip():
|
|
296
|
+
directory = Path(dir_input.strip())
|
|
297
|
+
|
|
298
|
+
target_dir = directory or Path.cwd()
|
|
299
|
+
if not target_dir.exists():
|
|
300
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
else:
|
|
302
|
+
# If directory is not empty and no force, abort
|
|
303
|
+
if any(target_dir.iterdir()) and not force:
|
|
304
|
+
click.echo(
|
|
305
|
+
"✗ Target directory is not empty. Use --force to initialize anyway.",
|
|
306
|
+
err=True,
|
|
307
|
+
)
|
|
308
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
309
|
+
|
|
310
|
+
repo = TEMPLATE_REPO
|
|
311
|
+
branch = TEMPLATE_BRANCH
|
|
312
|
+
subfolder = TEMPLATE_SUBFOLDERS[language_norm]
|
|
313
|
+
click.echo(f"Downloading {language_norm} template from {repo}@{branch}:{subfolder} ...")
|
|
314
|
+
download_and_extract_template(language_norm, target_dir)
|
|
315
|
+
click.echo("✓ Template files created.")
|
|
316
|
+
|
|
317
|
+
# Print next steps (no automatic install/build)
|
|
318
|
+
click.echo("\nNext steps:")
|
|
319
|
+
rel = target_dir.resolve()
|
|
320
|
+
if language_norm == "typescript":
|
|
321
|
+
click.echo(f" 1. cd {rel}")
|
|
322
|
+
click.echo(" 2. npm install")
|
|
323
|
+
click.echo(" 3. npm run build")
|
|
324
|
+
click.echo(
|
|
325
|
+
" 4. Use 'slcli function manage create' to register your compiled dist/main.wasm"
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
click.echo(f" 1. cd {rel}")
|
|
329
|
+
click.echo(" 2. (Optional) python -m venv .venv && source .venv/bin/activate")
|
|
330
|
+
click.echo(" 3. pip install -r requirements.txt (if provided)")
|
|
331
|
+
click.echo(
|
|
332
|
+
" 4. Use 'slcli function manage create' to register your function per README"
|
|
333
|
+
)
|
|
334
|
+
sys.exit(ExitCodes.SUCCESS)
|
|
335
|
+
except SystemExit: # re-raise explicit exits
|
|
336
|
+
raise
|
|
337
|
+
except Exception as exc: # noqa: BLE001
|
|
338
|
+
handle_api_error(exc)
|
|
339
|
+
|
|
340
|
+
# Function Execution Commands Group
|
|
341
|
+
@function.group(name="execute")
|
|
342
|
+
def execute_group() -> None:
|
|
343
|
+
"""Execute and manage function executions."""
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# Function Management Commands Group
|
|
347
|
+
@function.group(name="manage")
|
|
348
|
+
def manage_group() -> None:
|
|
349
|
+
"""Manage function definitions."""
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
@manage_group.command(name="list")
|
|
353
|
+
@click.option(
|
|
354
|
+
"--workspace",
|
|
355
|
+
"-w",
|
|
356
|
+
help="Filter by workspace name or ID",
|
|
357
|
+
)
|
|
358
|
+
@click.option(
|
|
359
|
+
"--name",
|
|
360
|
+
"-n",
|
|
361
|
+
help="Filter by function name (starts with pattern)",
|
|
362
|
+
)
|
|
363
|
+
@click.option(
|
|
364
|
+
"--interface-contains",
|
|
365
|
+
help="Filter by interface content (searches interface property for text)",
|
|
366
|
+
)
|
|
367
|
+
@click.option(
|
|
368
|
+
"--filter",
|
|
369
|
+
help='Custom Dynamic LINQ filter expression for advanced filtering. Examples: name.StartsWith("data") && interface.Contains("entrypoint")',
|
|
370
|
+
)
|
|
371
|
+
@click.option(
|
|
372
|
+
"--take",
|
|
373
|
+
"-t",
|
|
374
|
+
type=int,
|
|
375
|
+
default=25,
|
|
376
|
+
show_default=True,
|
|
377
|
+
help="Maximum number of functions to return",
|
|
378
|
+
)
|
|
379
|
+
@click.option(
|
|
380
|
+
"--format",
|
|
381
|
+
"-f",
|
|
382
|
+
type=click.Choice(["table", "json"]),
|
|
383
|
+
default="table",
|
|
384
|
+
show_default=True,
|
|
385
|
+
help="Output format",
|
|
386
|
+
)
|
|
387
|
+
def list_functions(
|
|
388
|
+
workspace: Optional[str] = None,
|
|
389
|
+
name: Optional[str] = None,
|
|
390
|
+
interface_contains: Optional[str] = None,
|
|
391
|
+
filter: Optional[str] = None,
|
|
392
|
+
take: int = 25,
|
|
393
|
+
format: str = "table",
|
|
394
|
+
) -> None:
|
|
395
|
+
"""List function definitions."""
|
|
396
|
+
format_output = validate_output_format(format)
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
workspace_map = get_workspace_map()
|
|
400
|
+
|
|
401
|
+
# Resolve workspace filter to ID if specified
|
|
402
|
+
workspace_id = None
|
|
403
|
+
workspace = get_effective_workspace(workspace)
|
|
404
|
+
if workspace:
|
|
405
|
+
workspace_id = resolve_workspace_filter(workspace, workspace_map)
|
|
406
|
+
|
|
407
|
+
# Use continuation token pagination to get all functions
|
|
408
|
+
all_functions = _query_all_functions(
|
|
409
|
+
workspace_filter=workspace_id,
|
|
410
|
+
name_filter=name,
|
|
411
|
+
interface_filter=interface_contains,
|
|
412
|
+
custom_filter=filter,
|
|
413
|
+
workspace_map=workspace_map,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# Create a mock response with all data
|
|
417
|
+
resp: Any = FilteredResponse({"functions": all_functions})
|
|
418
|
+
|
|
419
|
+
# Use universal response handler with function formatter
|
|
420
|
+
def function_formatter(function: Dict[str, Any]) -> List[str]:
|
|
421
|
+
ws_guid = function.get("workspaceId", "")
|
|
422
|
+
ws_name = get_workspace_display_name(ws_guid, workspace_map)
|
|
423
|
+
|
|
424
|
+
# Format timestamps
|
|
425
|
+
created_at = function.get("createdAt", "")
|
|
426
|
+
if created_at:
|
|
427
|
+
created_at = created_at.split("T")[0] # Just the date part
|
|
428
|
+
|
|
429
|
+
return [
|
|
430
|
+
function.get("id", ""),
|
|
431
|
+
function.get("name", ""),
|
|
432
|
+
function.get("version", ""),
|
|
433
|
+
ws_name,
|
|
434
|
+
created_at,
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
UniversalResponseHandler.handle_list_response(
|
|
438
|
+
resp=resp,
|
|
439
|
+
data_key="functions",
|
|
440
|
+
item_name="function",
|
|
441
|
+
format_output=format_output,
|
|
442
|
+
formatter_func=function_formatter,
|
|
443
|
+
headers=[
|
|
444
|
+
"ID",
|
|
445
|
+
"Name",
|
|
446
|
+
"Version",
|
|
447
|
+
"Workspace",
|
|
448
|
+
"Created",
|
|
449
|
+
],
|
|
450
|
+
column_widths=[36, 30, 10, 20, 12],
|
|
451
|
+
empty_message="No function definitions found.",
|
|
452
|
+
enable_pagination=True,
|
|
453
|
+
page_size=take,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
except Exception as exc:
|
|
457
|
+
handle_api_error(exc)
|
|
458
|
+
|
|
459
|
+
@manage_group.command(name="get")
|
|
460
|
+
@click.option(
|
|
461
|
+
"--id",
|
|
462
|
+
"-i",
|
|
463
|
+
"function_id",
|
|
464
|
+
required=True,
|
|
465
|
+
help="Function ID to retrieve",
|
|
466
|
+
)
|
|
467
|
+
@click.option(
|
|
468
|
+
"--format",
|
|
469
|
+
"-f",
|
|
470
|
+
type=click.Choice(["table", "json"]),
|
|
471
|
+
default="table",
|
|
472
|
+
show_default=True,
|
|
473
|
+
help="Output format",
|
|
474
|
+
)
|
|
475
|
+
def get_function(function_id: str, format: str = "table") -> None:
|
|
476
|
+
"""Get detailed information about a specific function definition."""
|
|
477
|
+
format_output = validate_output_format(format)
|
|
478
|
+
url = f"{get_unified_v2_base()}/functions/{function_id}"
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
resp = make_api_request("GET", url)
|
|
482
|
+
data = resp.json()
|
|
483
|
+
|
|
484
|
+
if format_output == "json":
|
|
485
|
+
click.echo(json.dumps(data, indent=2))
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
workspace_map = get_workspace_map()
|
|
489
|
+
ws_name = get_workspace_display_name(data.get("workspaceId", ""), workspace_map)
|
|
490
|
+
|
|
491
|
+
click.echo("Function Definition Details:")
|
|
492
|
+
click.echo("=" * 50)
|
|
493
|
+
click.echo(f"ID: {data.get('id', 'N/A')}")
|
|
494
|
+
click.echo(f"Name: {data.get('name', 'N/A')}")
|
|
495
|
+
click.echo(f"Description: {data.get('description', 'N/A')}")
|
|
496
|
+
click.echo(f"Workspace: {ws_name}")
|
|
497
|
+
click.echo(f"Version: {data.get('version', 'N/A')}")
|
|
498
|
+
click.echo(f"Runtime: {data.get('runtime', 'N/A')}")
|
|
499
|
+
click.echo(f"Created At: {data.get('createdAt', 'N/A')}")
|
|
500
|
+
click.echo(f"Updated At: {data.get('updatedAt', 'N/A')}")
|
|
501
|
+
|
|
502
|
+
interface = data.get("interface")
|
|
503
|
+
if interface:
|
|
504
|
+
# New-style interface (HTTP-like) with endpoints summary
|
|
505
|
+
endpoints = interface.get("endpoints")
|
|
506
|
+
if endpoints and isinstance(endpoints, list):
|
|
507
|
+
click.echo("\nInterface:")
|
|
508
|
+
default_path = interface.get("defaultPath")
|
|
509
|
+
if default_path:
|
|
510
|
+
click.echo(f"Default Path: {default_path}")
|
|
511
|
+
click.echo("Endpoints:")
|
|
512
|
+
for ep in endpoints:
|
|
513
|
+
methods = (
|
|
514
|
+
",".join(ep.get("methods", [])).upper() if ep.get("methods") else "*"
|
|
515
|
+
)
|
|
516
|
+
path = ep.get("path", "")
|
|
517
|
+
desc = ep.get("description", "")
|
|
518
|
+
click.echo(f" - {methods} {path} - {desc}")
|
|
519
|
+
# Legacy-style interface fields
|
|
520
|
+
if interface.get("entrypoint"):
|
|
521
|
+
click.echo(f"Entrypoint: {interface['entrypoint']}")
|
|
522
|
+
if interface.get("parameters"):
|
|
523
|
+
click.echo("\nParameters Schema:")
|
|
524
|
+
click.echo(json.dumps(interface["parameters"], indent=2))
|
|
525
|
+
if interface.get("returns"):
|
|
526
|
+
click.echo("\nReturns Schema:")
|
|
527
|
+
click.echo(json.dumps(interface["returns"], indent=2))
|
|
528
|
+
else:
|
|
529
|
+
if data.get("entrypoint"):
|
|
530
|
+
click.echo(f"Entrypoint: {data['entrypoint']}")
|
|
531
|
+
if data.get("parameters"):
|
|
532
|
+
click.echo("\nParameters Schema:")
|
|
533
|
+
click.echo(json.dumps(data["parameters"], indent=2))
|
|
534
|
+
if data.get("returns"):
|
|
535
|
+
click.echo("\nReturns Schema:")
|
|
536
|
+
click.echo(json.dumps(data["returns"], indent=2))
|
|
537
|
+
|
|
538
|
+
if data.get("properties"):
|
|
539
|
+
click.echo("\nCustom Properties:")
|
|
540
|
+
for key, value in data["properties"].items():
|
|
541
|
+
click.echo(f" {key}: {value}")
|
|
542
|
+
except Exception as exc:
|
|
543
|
+
handle_api_error(exc)
|
|
544
|
+
|
|
545
|
+
@manage_group.command(name="create")
|
|
546
|
+
@click.option(
|
|
547
|
+
"--name",
|
|
548
|
+
"-n",
|
|
549
|
+
required=True,
|
|
550
|
+
help="Function display name",
|
|
551
|
+
)
|
|
552
|
+
@click.option(
|
|
553
|
+
"--workspace",
|
|
554
|
+
"-w",
|
|
555
|
+
default="Default",
|
|
556
|
+
help="Workspace name or ID (default: 'Default')",
|
|
557
|
+
)
|
|
558
|
+
@click.option(
|
|
559
|
+
"--runtime",
|
|
560
|
+
"-r",
|
|
561
|
+
default="wasm",
|
|
562
|
+
type=click.Choice(["wasm"], case_sensitive=False),
|
|
563
|
+
help="Runtime environment for the function (WebAssembly)",
|
|
564
|
+
)
|
|
565
|
+
@click.option(
|
|
566
|
+
"--description",
|
|
567
|
+
"-d",
|
|
568
|
+
help="Function description",
|
|
569
|
+
)
|
|
570
|
+
@click.option(
|
|
571
|
+
"--version",
|
|
572
|
+
"-v",
|
|
573
|
+
default="1.0.0",
|
|
574
|
+
show_default=True,
|
|
575
|
+
help="Function version",
|
|
576
|
+
)
|
|
577
|
+
@click.option(
|
|
578
|
+
"--entrypoint",
|
|
579
|
+
"-e",
|
|
580
|
+
help="WASM file name without extension (stored in interface.entrypoint)",
|
|
581
|
+
)
|
|
582
|
+
@click.option(
|
|
583
|
+
"--content",
|
|
584
|
+
"-c",
|
|
585
|
+
help="Function source code content or file path",
|
|
586
|
+
)
|
|
587
|
+
@click.option(
|
|
588
|
+
"--parameters-schema",
|
|
589
|
+
"-p",
|
|
590
|
+
help="JSON schema for function parameters (stored in interface.parameters) (JSON string or file path)",
|
|
591
|
+
)
|
|
592
|
+
@click.option(
|
|
593
|
+
"--returns-schema",
|
|
594
|
+
help="JSON schema for function return value (stored in interface.returns) (JSON string or file path)",
|
|
595
|
+
)
|
|
596
|
+
@click.option(
|
|
597
|
+
"--properties",
|
|
598
|
+
help='Custom properties as JSON string for metadata and filtering (e.g., \'{"category": "processing", "team": "data-science"}\')',
|
|
599
|
+
)
|
|
600
|
+
def create_function(
|
|
601
|
+
name: str,
|
|
602
|
+
workspace: str = "Default",
|
|
603
|
+
runtime: str = "wasm",
|
|
604
|
+
description: Optional[str] = None,
|
|
605
|
+
version: str = "1.0.0",
|
|
606
|
+
entrypoint: Optional[str] = None,
|
|
607
|
+
content: Optional[str] = None,
|
|
608
|
+
parameters_schema: Optional[str] = None,
|
|
609
|
+
returns_schema: Optional[str] = None,
|
|
610
|
+
properties: Optional[str] = None,
|
|
611
|
+
) -> None:
|
|
612
|
+
"""Create a new function definition with metadata for efficient querying."""
|
|
613
|
+
from .utils import check_readonly_mode
|
|
614
|
+
|
|
615
|
+
check_readonly_mode("create a function")
|
|
616
|
+
|
|
617
|
+
url = f"{get_unified_v2_base()}/functions"
|
|
618
|
+
try:
|
|
619
|
+
workspace_id = get_workspace_id_with_fallback(
|
|
620
|
+
get_effective_workspace(workspace) or workspace
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
custom_properties: Dict[str, Any] = {}
|
|
624
|
+
if properties:
|
|
625
|
+
try:
|
|
626
|
+
custom_properties.update(json.loads(properties))
|
|
627
|
+
except json.JSONDecodeError:
|
|
628
|
+
click.echo("✗ Error: Invalid JSON in --properties option", err=True)
|
|
629
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
630
|
+
|
|
631
|
+
params_schema = None
|
|
632
|
+
if parameters_schema:
|
|
633
|
+
try:
|
|
634
|
+
params_schema = (
|
|
635
|
+
json.loads(parameters_schema)
|
|
636
|
+
if parameters_schema.startswith("{")
|
|
637
|
+
else load_json_file(parameters_schema)
|
|
638
|
+
)
|
|
639
|
+
except Exception as e: # noqa: BLE001
|
|
640
|
+
click.echo(f"✗ Error loading parameters schema: {e}", err=True)
|
|
641
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
642
|
+
|
|
643
|
+
ret_schema = None
|
|
644
|
+
if returns_schema:
|
|
645
|
+
try:
|
|
646
|
+
ret_schema = (
|
|
647
|
+
json.loads(returns_schema)
|
|
648
|
+
if returns_schema.startswith("{")
|
|
649
|
+
else load_json_file(returns_schema)
|
|
650
|
+
)
|
|
651
|
+
except Exception as e: # noqa: BLE001
|
|
652
|
+
click.echo(f"✗ Error loading returns schema: {e}", err=True)
|
|
653
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
654
|
+
|
|
655
|
+
if not entrypoint and content and Path(content).exists():
|
|
656
|
+
entrypoint = Path(content).stem
|
|
657
|
+
|
|
658
|
+
interface_obj: Optional[Dict[str, Any]] = None
|
|
659
|
+
if entrypoint or params_schema or ret_schema:
|
|
660
|
+
interface_obj = {}
|
|
661
|
+
if entrypoint:
|
|
662
|
+
interface_obj["entrypoint"] = entrypoint
|
|
663
|
+
if params_schema:
|
|
664
|
+
interface_obj["parameters"] = params_schema
|
|
665
|
+
if ret_schema:
|
|
666
|
+
interface_obj["returns"] = ret_schema
|
|
667
|
+
|
|
668
|
+
if content:
|
|
669
|
+
if Path(content).exists():
|
|
670
|
+
try:
|
|
671
|
+
with open(content, "rb") as f:
|
|
672
|
+
content_data = f.read()
|
|
673
|
+
except Exception as e: # noqa: BLE001
|
|
674
|
+
click.echo(f"✗ Error reading content file: {e}", err=True)
|
|
675
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
676
|
+
else:
|
|
677
|
+
content_data = content.encode("utf-8")
|
|
678
|
+
|
|
679
|
+
function_metadata: Dict[str, Any] = {
|
|
680
|
+
"name": name,
|
|
681
|
+
"workspaceId": workspace_id,
|
|
682
|
+
"runtime": runtime.lower(),
|
|
683
|
+
"version": version,
|
|
684
|
+
}
|
|
685
|
+
if description:
|
|
686
|
+
function_metadata["description"] = description
|
|
687
|
+
if interface_obj:
|
|
688
|
+
function_metadata["interface"] = interface_obj
|
|
689
|
+
if custom_properties:
|
|
690
|
+
function_metadata["properties"] = custom_properties
|
|
691
|
+
|
|
692
|
+
files = {
|
|
693
|
+
"metadata": (None, json.dumps(function_metadata), "application/json"),
|
|
694
|
+
"content": ("function_content", content_data, "application/octet-stream"),
|
|
695
|
+
}
|
|
696
|
+
resp = requests.post(
|
|
697
|
+
url,
|
|
698
|
+
files=files, # type: ignore
|
|
699
|
+
headers=get_headers(""),
|
|
700
|
+
verify=get_ssl_verify(),
|
|
701
|
+
)
|
|
702
|
+
resp.raise_for_status()
|
|
703
|
+
else:
|
|
704
|
+
function_request: Dict[str, Any] = {
|
|
705
|
+
"name": name,
|
|
706
|
+
"workspaceId": workspace_id,
|
|
707
|
+
"runtime": runtime.lower(),
|
|
708
|
+
"version": version,
|
|
709
|
+
}
|
|
710
|
+
if description:
|
|
711
|
+
function_request["description"] = description
|
|
712
|
+
if interface_obj:
|
|
713
|
+
function_request["interface"] = interface_obj
|
|
714
|
+
if custom_properties:
|
|
715
|
+
function_request["properties"] = custom_properties
|
|
716
|
+
resp = make_api_request("POST", url, function_request)
|
|
717
|
+
|
|
718
|
+
response_data = resp.json()
|
|
719
|
+
click.echo(
|
|
720
|
+
f"✓ Function definition created successfully with ID: {response_data.get('id', '')}"
|
|
721
|
+
)
|
|
722
|
+
except Exception as exc: # noqa: BLE001
|
|
723
|
+
handle_api_error(exc)
|
|
724
|
+
|
|
725
|
+
@manage_group.command(name="update")
|
|
726
|
+
@click.option(
|
|
727
|
+
"--id",
|
|
728
|
+
"-i",
|
|
729
|
+
"function_id",
|
|
730
|
+
required=True,
|
|
731
|
+
help="Function ID to update",
|
|
732
|
+
)
|
|
733
|
+
@click.option(
|
|
734
|
+
"--name",
|
|
735
|
+
"-n",
|
|
736
|
+
help="Updated function display name",
|
|
737
|
+
)
|
|
738
|
+
@click.option(
|
|
739
|
+
"--description",
|
|
740
|
+
"-d",
|
|
741
|
+
help="Updated function description",
|
|
742
|
+
)
|
|
743
|
+
@click.option(
|
|
744
|
+
"--version",
|
|
745
|
+
"-v",
|
|
746
|
+
help="Updated function version",
|
|
747
|
+
)
|
|
748
|
+
@click.option(
|
|
749
|
+
"--workspace",
|
|
750
|
+
"-w",
|
|
751
|
+
help="Updated workspace for the function (name or ID)",
|
|
752
|
+
)
|
|
753
|
+
@click.option(
|
|
754
|
+
"--runtime",
|
|
755
|
+
help="Updated runtime environment (default: wasm)",
|
|
756
|
+
default="wasm",
|
|
757
|
+
)
|
|
758
|
+
@click.option(
|
|
759
|
+
"--entrypoint",
|
|
760
|
+
"-e",
|
|
761
|
+
help="Updated WASM file name without extension (stored in interface.entrypoint)",
|
|
762
|
+
)
|
|
763
|
+
@click.option(
|
|
764
|
+
"--content",
|
|
765
|
+
"-c",
|
|
766
|
+
help="Updated function source code content or file path",
|
|
767
|
+
)
|
|
768
|
+
@click.option(
|
|
769
|
+
"--parameters-schema",
|
|
770
|
+
"-p",
|
|
771
|
+
help="Updated JSON schema for function parameters (stored in interface.parameters) (JSON string or file path)",
|
|
772
|
+
)
|
|
773
|
+
@click.option(
|
|
774
|
+
"--returns-schema",
|
|
775
|
+
help="Updated JSON schema for function return value (stored in interface.returns) (JSON string or file path)",
|
|
776
|
+
)
|
|
777
|
+
@click.option(
|
|
778
|
+
"--properties",
|
|
779
|
+
help="Updated custom properties as JSON string for metadata and filtering (replaces existing properties)",
|
|
780
|
+
)
|
|
781
|
+
def update_function(
|
|
782
|
+
function_id: str,
|
|
783
|
+
name: Optional[str] = None,
|
|
784
|
+
description: Optional[str] = None,
|
|
785
|
+
version: Optional[str] = None,
|
|
786
|
+
workspace: Optional[str] = None,
|
|
787
|
+
runtime: str = "wasm",
|
|
788
|
+
entrypoint: Optional[str] = None,
|
|
789
|
+
content: Optional[str] = None,
|
|
790
|
+
parameters_schema: Optional[str] = None,
|
|
791
|
+
returns_schema: Optional[str] = None,
|
|
792
|
+
properties: Optional[str] = None,
|
|
793
|
+
) -> None:
|
|
794
|
+
"""Update an existing function definition."""
|
|
795
|
+
from .utils import check_readonly_mode
|
|
796
|
+
|
|
797
|
+
check_readonly_mode("update a function")
|
|
798
|
+
|
|
799
|
+
url = f"{get_unified_v2_base()}/functions/{function_id}"
|
|
800
|
+
try:
|
|
801
|
+
existing_function = make_api_request("GET", url).json()
|
|
802
|
+
except Exception as e: # noqa: BLE001
|
|
803
|
+
click.echo(f"✗ Error fetching existing function: {e}", err=True)
|
|
804
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
805
|
+
|
|
806
|
+
try:
|
|
807
|
+
workspace_id = existing_function.get("workspaceId")
|
|
808
|
+
if workspace:
|
|
809
|
+
try:
|
|
810
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
811
|
+
except Exception as e: # noqa: BLE001
|
|
812
|
+
click.echo(f"✗ Error resolving workspace '{workspace}': {e}", err=True)
|
|
813
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
814
|
+
|
|
815
|
+
params_schema = None
|
|
816
|
+
if parameters_schema:
|
|
817
|
+
try:
|
|
818
|
+
params_schema = (
|
|
819
|
+
json.loads(parameters_schema)
|
|
820
|
+
if parameters_schema.startswith("{")
|
|
821
|
+
else load_json_file(parameters_schema)
|
|
822
|
+
)
|
|
823
|
+
except Exception as e: # noqa: BLE001
|
|
824
|
+
click.echo(f"✗ Error loading parameters schema: {e}", err=True)
|
|
825
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
826
|
+
|
|
827
|
+
ret_schema = None
|
|
828
|
+
if returns_schema:
|
|
829
|
+
try:
|
|
830
|
+
ret_schema = (
|
|
831
|
+
json.loads(returns_schema)
|
|
832
|
+
if returns_schema.startswith("{")
|
|
833
|
+
else load_json_file(returns_schema)
|
|
834
|
+
)
|
|
835
|
+
except Exception as e: # noqa: BLE001
|
|
836
|
+
click.echo(f"✗ Error loading returns schema: {e}", err=True)
|
|
837
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
838
|
+
|
|
839
|
+
interface_obj = (
|
|
840
|
+
existing_function.get("interface", {}).copy()
|
|
841
|
+
if existing_function.get("interface")
|
|
842
|
+
else {}
|
|
843
|
+
)
|
|
844
|
+
if entrypoint is not None:
|
|
845
|
+
interface_obj["entrypoint"] = entrypoint
|
|
846
|
+
if params_schema is not None:
|
|
847
|
+
interface_obj["parameters"] = params_schema
|
|
848
|
+
if ret_schema is not None:
|
|
849
|
+
interface_obj["returns"] = ret_schema
|
|
850
|
+
|
|
851
|
+
custom_properties = None
|
|
852
|
+
if properties:
|
|
853
|
+
try:
|
|
854
|
+
custom_properties = json.loads(properties)
|
|
855
|
+
except json.JSONDecodeError:
|
|
856
|
+
click.echo("✗ Error: Invalid JSON in --properties option", err=True)
|
|
857
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
858
|
+
|
|
859
|
+
if (
|
|
860
|
+
name is None
|
|
861
|
+
and description is None
|
|
862
|
+
and version is None
|
|
863
|
+
and workspace is None
|
|
864
|
+
and entrypoint is None
|
|
865
|
+
and content is None
|
|
866
|
+
and parameters_schema is None
|
|
867
|
+
and returns_schema is None
|
|
868
|
+
and properties is None
|
|
869
|
+
):
|
|
870
|
+
click.echo(
|
|
871
|
+
"✗ No updates provided. Please specify at least one field to update.", err=True
|
|
872
|
+
)
|
|
873
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
874
|
+
|
|
875
|
+
function_metadata: Dict[str, Any] = {
|
|
876
|
+
"name": name if name is not None else existing_function["name"],
|
|
877
|
+
"workspaceId": workspace_id,
|
|
878
|
+
"runtime": runtime,
|
|
879
|
+
}
|
|
880
|
+
if description is not None:
|
|
881
|
+
function_metadata["description"] = description
|
|
882
|
+
elif existing_function.get("description") is not None:
|
|
883
|
+
function_metadata["description"] = existing_function["description"]
|
|
884
|
+
if version is not None:
|
|
885
|
+
function_metadata["version"] = version
|
|
886
|
+
elif existing_function.get("version"):
|
|
887
|
+
function_metadata["version"] = existing_function["version"]
|
|
888
|
+
if interface_obj:
|
|
889
|
+
function_metadata["interface"] = interface_obj
|
|
890
|
+
if custom_properties is not None:
|
|
891
|
+
function_metadata["properties"] = custom_properties
|
|
892
|
+
elif existing_function.get("properties"):
|
|
893
|
+
function_metadata["properties"] = existing_function["properties"]
|
|
894
|
+
|
|
895
|
+
content_data = None
|
|
896
|
+
if content:
|
|
897
|
+
if Path(content).exists():
|
|
898
|
+
try:
|
|
899
|
+
with open(content, "rb") as f:
|
|
900
|
+
content_data = f.read()
|
|
901
|
+
except Exception as e: # noqa: BLE001
|
|
902
|
+
click.echo(f"✗ Error reading content file: {e}", err=True)
|
|
903
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
904
|
+
else:
|
|
905
|
+
content_data = content.encode("utf-8")
|
|
906
|
+
|
|
907
|
+
files: Dict[str, Any] = {
|
|
908
|
+
"metadata": (None, json.dumps(function_metadata), "application/json"),
|
|
909
|
+
}
|
|
910
|
+
if content_data is not None:
|
|
911
|
+
files["content"] = ("function_content", content_data, "application/octet-stream")
|
|
912
|
+
|
|
913
|
+
resp = requests.put(
|
|
914
|
+
url,
|
|
915
|
+
files=files,
|
|
916
|
+
headers=get_headers(""),
|
|
917
|
+
verify=get_ssl_verify(),
|
|
918
|
+
)
|
|
919
|
+
resp.raise_for_status()
|
|
920
|
+
click.echo("✓ Function definition updated successfully")
|
|
921
|
+
except Exception as exc: # noqa: BLE001
|
|
922
|
+
handle_api_error(exc)
|
|
923
|
+
|
|
924
|
+
@manage_group.command(name="delete")
|
|
925
|
+
@click.option(
|
|
926
|
+
"--id",
|
|
927
|
+
"-i",
|
|
928
|
+
"function_id",
|
|
929
|
+
required=True,
|
|
930
|
+
help="Function ID to delete",
|
|
931
|
+
)
|
|
932
|
+
@click.option(
|
|
933
|
+
"--force",
|
|
934
|
+
is_flag=True,
|
|
935
|
+
help="Skip confirmation prompt",
|
|
936
|
+
)
|
|
937
|
+
def delete_function(function_id: str, force: bool = False) -> None:
|
|
938
|
+
"""Delete a function definition."""
|
|
939
|
+
from .utils import check_readonly_mode
|
|
940
|
+
|
|
941
|
+
check_readonly_mode("delete a function")
|
|
942
|
+
|
|
943
|
+
url = f"{get_unified_v2_base()}/functions/{function_id}"
|
|
944
|
+
try:
|
|
945
|
+
if not force and not click.confirm(
|
|
946
|
+
f"Are you sure you want to delete function {function_id}?"
|
|
947
|
+
):
|
|
948
|
+
click.echo("Function deletion cancelled.")
|
|
949
|
+
return
|
|
950
|
+
resp = make_api_request("DELETE", url, handle_errors=False)
|
|
951
|
+
if resp.status_code == 204:
|
|
952
|
+
click.echo(f"✓ Function {function_id} deleted successfully.")
|
|
953
|
+
else:
|
|
954
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
955
|
+
display_api_errors("Function deletion failed", response_data, detailed=True)
|
|
956
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
957
|
+
except Exception as exc: # noqa: BLE001
|
|
958
|
+
handle_api_error(exc)
|
|
959
|
+
|
|
960
|
+
@manage_group.command(name="download-content")
|
|
961
|
+
@click.option(
|
|
962
|
+
"--id",
|
|
963
|
+
"-i",
|
|
964
|
+
"function_id",
|
|
965
|
+
required=True,
|
|
966
|
+
help="Function ID to download content from",
|
|
967
|
+
)
|
|
968
|
+
@click.option(
|
|
969
|
+
"--output",
|
|
970
|
+
"-o",
|
|
971
|
+
help="Output file path (defaults to function_<id> with appropriate extension)",
|
|
972
|
+
)
|
|
973
|
+
def download_function_content(function_id: str, output: Optional[str] = None) -> None:
|
|
974
|
+
"""Download function source code content."""
|
|
975
|
+
url = f"{get_unified_v2_base()}/functions/{function_id}/content"
|
|
976
|
+
try:
|
|
977
|
+
resp = make_api_request("GET", url, handle_errors=False)
|
|
978
|
+
if resp.status_code != 200:
|
|
979
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
980
|
+
display_api_errors("Function content download failed", response_data, detailed=True)
|
|
981
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
982
|
+
|
|
983
|
+
if not output:
|
|
984
|
+
try:
|
|
985
|
+
meta_data = make_api_request(
|
|
986
|
+
"GET", f"{get_unified_v2_base()}/functions/{function_id}"
|
|
987
|
+
).json()
|
|
988
|
+
runtime = meta_data.get("runtime", "").lower()
|
|
989
|
+
ext = {"wasm": ".wasm"}.get(runtime, ".wasm")
|
|
990
|
+
output = f"function_{function_id}{ext}"
|
|
991
|
+
except Exception: # noqa: BLE001
|
|
992
|
+
output = f"function_{function_id}.wasm"
|
|
993
|
+
|
|
994
|
+
with open(output, "wb") as f:
|
|
995
|
+
f.write(resp.content)
|
|
996
|
+
click.echo(f"✓ Function content downloaded to '{output}' ({len(resp.content)} bytes)")
|
|
997
|
+
except Exception as exc: # noqa: BLE001
|
|
998
|
+
handle_api_error(exc)
|
|
999
|
+
|
|
1000
|
+
# Function Execution Management Commands
|
|
1001
|
+
@execute_group.command(name="list")
|
|
1002
|
+
@click.option(
|
|
1003
|
+
"--workspace",
|
|
1004
|
+
"-w",
|
|
1005
|
+
help="Filter by workspace name or ID",
|
|
1006
|
+
)
|
|
1007
|
+
@click.option(
|
|
1008
|
+
"--status",
|
|
1009
|
+
"-s",
|
|
1010
|
+
type=click.Choice(
|
|
1011
|
+
["QUEUED", "RUNNING", "SUCCEEDED", "FAILED", "CANCELLED", "TIMEOUT"],
|
|
1012
|
+
case_sensitive=False,
|
|
1013
|
+
),
|
|
1014
|
+
help="Filter by execution status",
|
|
1015
|
+
)
|
|
1016
|
+
@click.option(
|
|
1017
|
+
"--function-id",
|
|
1018
|
+
"-f",
|
|
1019
|
+
help="Filter by function ID",
|
|
1020
|
+
)
|
|
1021
|
+
@click.option(
|
|
1022
|
+
"--take",
|
|
1023
|
+
"-t",
|
|
1024
|
+
type=int,
|
|
1025
|
+
default=25,
|
|
1026
|
+
show_default=True,
|
|
1027
|
+
help="Maximum number of executions to return",
|
|
1028
|
+
)
|
|
1029
|
+
@click.option(
|
|
1030
|
+
"--format",
|
|
1031
|
+
type=click.Choice(["table", "json"]),
|
|
1032
|
+
default="table",
|
|
1033
|
+
show_default=True,
|
|
1034
|
+
help="Output format",
|
|
1035
|
+
)
|
|
1036
|
+
def list_executions(
|
|
1037
|
+
workspace: Optional[str] = None,
|
|
1038
|
+
status: Optional[str] = None,
|
|
1039
|
+
function_id: Optional[str] = None,
|
|
1040
|
+
take: int = 25,
|
|
1041
|
+
format: str = "table",
|
|
1042
|
+
) -> None:
|
|
1043
|
+
"""List function executions."""
|
|
1044
|
+
format_output = validate_output_format(format)
|
|
1045
|
+
|
|
1046
|
+
try:
|
|
1047
|
+
workspace_map = get_workspace_map()
|
|
1048
|
+
|
|
1049
|
+
# Resolve workspace filter to ID if specified
|
|
1050
|
+
workspace_id = None
|
|
1051
|
+
workspace = get_effective_workspace(workspace)
|
|
1052
|
+
if workspace:
|
|
1053
|
+
workspace_id = resolve_workspace_filter(workspace, workspace_map)
|
|
1054
|
+
|
|
1055
|
+
# Normalize status to uppercase if provided
|
|
1056
|
+
status_filter = status.upper() if status else None
|
|
1057
|
+
|
|
1058
|
+
# Use continuation token pagination to get all executions
|
|
1059
|
+
all_executions = _query_all_executions(
|
|
1060
|
+
workspace_id, status_filter, function_id, workspace_map
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
# Create a mock response with all data
|
|
1064
|
+
resp: Any = FilteredResponse({"executions": all_executions})
|
|
1065
|
+
|
|
1066
|
+
# Use universal response handler with execution formatter
|
|
1067
|
+
def execution_formatter(execution: Dict[str, Any]) -> List[str]:
|
|
1068
|
+
ws_guid = execution.get("workspaceId", "")
|
|
1069
|
+
ws_name = get_workspace_display_name(ws_guid, workspace_map)
|
|
1070
|
+
|
|
1071
|
+
# Format timestamps
|
|
1072
|
+
queued_at = execution.get("queuedAt", "")
|
|
1073
|
+
if queued_at:
|
|
1074
|
+
queued_at = queued_at.split("T")[0] # Just the date part
|
|
1075
|
+
|
|
1076
|
+
return [
|
|
1077
|
+
execution.get("id", ""), # Full ID
|
|
1078
|
+
execution.get("functionId", ""), # Full function ID
|
|
1079
|
+
ws_name,
|
|
1080
|
+
execution.get("status", "UNKNOWN"),
|
|
1081
|
+
queued_at,
|
|
1082
|
+
]
|
|
1083
|
+
|
|
1084
|
+
UniversalResponseHandler.handle_list_response(
|
|
1085
|
+
resp=resp,
|
|
1086
|
+
data_key="executions",
|
|
1087
|
+
item_name="execution",
|
|
1088
|
+
format_output=format_output,
|
|
1089
|
+
formatter_func=execution_formatter,
|
|
1090
|
+
headers=["ID", "Function ID", "Workspace", "Status", "Queued"],
|
|
1091
|
+
column_widths=[36, 36, 20, 12, 12],
|
|
1092
|
+
empty_message="No function executions found.",
|
|
1093
|
+
enable_pagination=True,
|
|
1094
|
+
page_size=take,
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
except Exception as exc:
|
|
1098
|
+
handle_api_error(exc)
|
|
1099
|
+
|
|
1100
|
+
@execute_group.command(name="get")
|
|
1101
|
+
@click.option(
|
|
1102
|
+
"--id",
|
|
1103
|
+
"-i",
|
|
1104
|
+
"execution_id",
|
|
1105
|
+
required=True,
|
|
1106
|
+
help="Execution ID to retrieve",
|
|
1107
|
+
)
|
|
1108
|
+
@click.option(
|
|
1109
|
+
"--format",
|
|
1110
|
+
"-f",
|
|
1111
|
+
type=click.Choice(["table", "json"]),
|
|
1112
|
+
default="table",
|
|
1113
|
+
show_default=True,
|
|
1114
|
+
help="Output format",
|
|
1115
|
+
)
|
|
1116
|
+
def get_execution(execution_id: str, format: str = "table") -> None:
|
|
1117
|
+
"""Get detailed information about a specific function execution."""
|
|
1118
|
+
format_output = validate_output_format(format)
|
|
1119
|
+
url = f"{get_unified_v2_base()}/executions/{execution_id}"
|
|
1120
|
+
try:
|
|
1121
|
+
data = make_api_request("GET", url).json()
|
|
1122
|
+
if format_output == "json":
|
|
1123
|
+
click.echo(json.dumps(data, indent=2))
|
|
1124
|
+
return
|
|
1125
|
+
workspace_map = get_workspace_map()
|
|
1126
|
+
ws_name = get_workspace_display_name(data.get("workspaceId", ""), workspace_map)
|
|
1127
|
+
click.echo("Function Execution Details:")
|
|
1128
|
+
click.echo("=" * 50)
|
|
1129
|
+
click.echo(f"ID: {data.get('id', 'N/A')}")
|
|
1130
|
+
click.echo(f"Function ID: {data.get('functionId', 'N/A')}")
|
|
1131
|
+
click.echo(f"Workspace: {ws_name}")
|
|
1132
|
+
click.echo(f"Status: {data.get('status', 'N/A')}")
|
|
1133
|
+
click.echo(f"Timeout: {data.get('timeout', 'N/A')} seconds")
|
|
1134
|
+
click.echo(f"Retry Count: {data.get('retryCount', 0)}")
|
|
1135
|
+
click.echo(f"Cached Result: {data.get('cachedResult', False)}")
|
|
1136
|
+
click.echo(f"Queued At: {data.get('queuedAt', 'N/A')}")
|
|
1137
|
+
click.echo(f"Started At: {data.get('startedAt', 'N/A')}")
|
|
1138
|
+
click.echo(f"Completed At: {data.get('completedAt', 'N/A')}")
|
|
1139
|
+
if data.get("parameters"):
|
|
1140
|
+
click.echo("\nParameters:")
|
|
1141
|
+
click.echo(json.dumps(data["parameters"], indent=2))
|
|
1142
|
+
if data.get("result"):
|
|
1143
|
+
click.echo("\nResult:")
|
|
1144
|
+
click.echo(json.dumps(data["result"], indent=2))
|
|
1145
|
+
if data.get("errorMessage"):
|
|
1146
|
+
click.echo("\nError Message:")
|
|
1147
|
+
click.echo(data["errorMessage"])
|
|
1148
|
+
except Exception as exc: # noqa: BLE001
|
|
1149
|
+
handle_api_error(exc)
|
|
1150
|
+
|
|
1151
|
+
@execute_group.command(name="sync")
|
|
1152
|
+
@click.option(
|
|
1153
|
+
"--function-id",
|
|
1154
|
+
"-f",
|
|
1155
|
+
required=True,
|
|
1156
|
+
help="Function ID to execute synchronously",
|
|
1157
|
+
)
|
|
1158
|
+
@click.option(
|
|
1159
|
+
"--workspace",
|
|
1160
|
+
"-w",
|
|
1161
|
+
default="Default",
|
|
1162
|
+
help="Workspace name or ID (default: 'Default')",
|
|
1163
|
+
)
|
|
1164
|
+
@click.option(
|
|
1165
|
+
"--parameters",
|
|
1166
|
+
"-p",
|
|
1167
|
+
help="Raw JSON (string or file) for advanced parameters object (overrides --method/--path/--header/--body).",
|
|
1168
|
+
)
|
|
1169
|
+
@click.option(
|
|
1170
|
+
"--method",
|
|
1171
|
+
default="POST",
|
|
1172
|
+
show_default=True,
|
|
1173
|
+
help="Invocation HTTP method placed in parameters.method (ignored if --parameters used).",
|
|
1174
|
+
)
|
|
1175
|
+
@click.option(
|
|
1176
|
+
"--path",
|
|
1177
|
+
default="/invoke",
|
|
1178
|
+
show_default=True,
|
|
1179
|
+
help="Invocation path placed in parameters.path (ignored if --parameters used).",
|
|
1180
|
+
)
|
|
1181
|
+
@click.option(
|
|
1182
|
+
"--header",
|
|
1183
|
+
"-H",
|
|
1184
|
+
multiple=True,
|
|
1185
|
+
help="Request header key=value (can repeat). Ignored if --parameters used.",
|
|
1186
|
+
)
|
|
1187
|
+
@click.option(
|
|
1188
|
+
"--body",
|
|
1189
|
+
help="JSON string or file for request body placed in parameters.body (ignored if --parameters used).",
|
|
1190
|
+
)
|
|
1191
|
+
@click.option(
|
|
1192
|
+
"--timeout",
|
|
1193
|
+
"-t",
|
|
1194
|
+
type=int,
|
|
1195
|
+
default=300,
|
|
1196
|
+
show_default=True,
|
|
1197
|
+
help="Execution timeout in seconds (0 for infinite, maximum 3600 for synchronous execution)",
|
|
1198
|
+
)
|
|
1199
|
+
@click.option(
|
|
1200
|
+
"--client-request-id",
|
|
1201
|
+
help="Client-provided unique identifier for tracking",
|
|
1202
|
+
)
|
|
1203
|
+
@click.option(
|
|
1204
|
+
"--format",
|
|
1205
|
+
type=click.Choice(["table", "json"]),
|
|
1206
|
+
default="table",
|
|
1207
|
+
show_default=True,
|
|
1208
|
+
help="Output format",
|
|
1209
|
+
)
|
|
1210
|
+
def execute_function(
|
|
1211
|
+
function_id: str,
|
|
1212
|
+
workspace: str = "Default",
|
|
1213
|
+
parameters: Optional[str] = None,
|
|
1214
|
+
method: str = "POST",
|
|
1215
|
+
path: str = "/invoke",
|
|
1216
|
+
header: Optional[tuple] = None,
|
|
1217
|
+
body: Optional[str] = None,
|
|
1218
|
+
timeout: int = 300,
|
|
1219
|
+
client_request_id: Optional[str] = None,
|
|
1220
|
+
format: str = "table",
|
|
1221
|
+
) -> None:
|
|
1222
|
+
"""Execute a function synchronously and return the result.
|
|
1223
|
+
|
|
1224
|
+
This sends a single request and waits for completion (no async polling).
|
|
1225
|
+
"""
|
|
1226
|
+
format_output = validate_output_format(format)
|
|
1227
|
+
url = f"{get_unified_v2_base()}/functions/{function_id}/execute"
|
|
1228
|
+
try:
|
|
1229
|
+
execution_parameters: Dict[str, Any] = {}
|
|
1230
|
+
if parameters:
|
|
1231
|
+
try:
|
|
1232
|
+
execution_parameters = (
|
|
1233
|
+
json.loads(parameters)
|
|
1234
|
+
if parameters.strip().startswith("{")
|
|
1235
|
+
else load_json_file(parameters)
|
|
1236
|
+
)
|
|
1237
|
+
except Exception as e: # noqa: BLE001
|
|
1238
|
+
click.echo(f"✗ Error parsing parameters: {e}", err=True)
|
|
1239
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
1240
|
+
legacy_keys = {"method", "path", "headers", "body"}
|
|
1241
|
+
if not any(k in execution_parameters for k in legacy_keys):
|
|
1242
|
+
execution_parameters = {"body": execution_parameters}
|
|
1243
|
+
else:
|
|
1244
|
+
# Determine if user explicitly set any of the four HTTP-related flags.
|
|
1245
|
+
# We treat them as specified only if they differ from defaults or were
|
|
1246
|
+
# provided via the parameters option.
|
|
1247
|
+
user_provided_any = (
|
|
1248
|
+
bool(header)
|
|
1249
|
+
or body is not None
|
|
1250
|
+
or (method.upper() != "POST" or path != "/invoke")
|
|
1251
|
+
)
|
|
1252
|
+
if not user_provided_any:
|
|
1253
|
+
# Pure omission: apply fallback default GET /
|
|
1254
|
+
execution_parameters = {"method": "GET", "path": "/"}
|
|
1255
|
+
else:
|
|
1256
|
+
headers_dict: Dict[str, str] = {}
|
|
1257
|
+
if header:
|
|
1258
|
+
for h in header:
|
|
1259
|
+
if "=" not in h:
|
|
1260
|
+
click.echo(
|
|
1261
|
+
f"✗ Invalid header format (expected key=value): {h}",
|
|
1262
|
+
err=True,
|
|
1263
|
+
)
|
|
1264
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
1265
|
+
k, v = h.split("=", 1)
|
|
1266
|
+
headers_dict[k.strip()] = v.strip()
|
|
1267
|
+
body_value: Any = None
|
|
1268
|
+
if body:
|
|
1269
|
+
try:
|
|
1270
|
+
body_value = (
|
|
1271
|
+
json.loads(body)
|
|
1272
|
+
if body.strip().startswith("{") or body.strip().startswith("[")
|
|
1273
|
+
else load_json_file(body)
|
|
1274
|
+
)
|
|
1275
|
+
except Exception:
|
|
1276
|
+
body_value = body
|
|
1277
|
+
norm_path = path if path.startswith("/") else f"/{path}"
|
|
1278
|
+
execution_parameters = {
|
|
1279
|
+
"method": method.upper(),
|
|
1280
|
+
"path": norm_path,
|
|
1281
|
+
}
|
|
1282
|
+
if headers_dict:
|
|
1283
|
+
execution_parameters["headers"] = headers_dict
|
|
1284
|
+
if body_value is not None:
|
|
1285
|
+
execution_parameters["body"] = body_value
|
|
1286
|
+
if timeout > 3600:
|
|
1287
|
+
click.echo(
|
|
1288
|
+
"✗ Timeout cannot exceed 3600 seconds (1 hour) for synchronous execution",
|
|
1289
|
+
err=True,
|
|
1290
|
+
)
|
|
1291
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
1292
|
+
execute_request: Dict[str, Any] = {
|
|
1293
|
+
"parameters": execution_parameters,
|
|
1294
|
+
"timeout": timeout,
|
|
1295
|
+
"async": False,
|
|
1296
|
+
}
|
|
1297
|
+
if client_request_id:
|
|
1298
|
+
execute_request["clientRequestId"] = client_request_id
|
|
1299
|
+
response_data = make_api_request("POST", url, execute_request).json()
|
|
1300
|
+
if format_output == "json":
|
|
1301
|
+
click.echo(json.dumps(response_data, indent=2))
|
|
1302
|
+
return
|
|
1303
|
+
click.echo("Function Execution Completed:")
|
|
1304
|
+
click.echo("=" * 50)
|
|
1305
|
+
click.echo(f"Execution ID: {response_data.get('executionId', 'N/A')}")
|
|
1306
|
+
click.echo(f"Execution Time: {response_data.get('executionTime', 0)} ms")
|
|
1307
|
+
click.echo(f"Cached Result: {response_data.get('cachedResult', False)}")
|
|
1308
|
+
result = response_data.get("result")
|
|
1309
|
+
if result is not None:
|
|
1310
|
+
click.echo("\nResult:")
|
|
1311
|
+
click.echo(json.dumps(result, indent=2))
|
|
1312
|
+
else:
|
|
1313
|
+
click.echo("\nResult: None (no return value)")
|
|
1314
|
+
except Exception as exc: # noqa: BLE001
|
|
1315
|
+
handle_api_error(exc)
|
|
1316
|
+
|
|
1317
|
+
@execute_group.command(name="cancel")
|
|
1318
|
+
@click.option(
|
|
1319
|
+
"--id",
|
|
1320
|
+
"-i",
|
|
1321
|
+
"execution_ids",
|
|
1322
|
+
multiple=True,
|
|
1323
|
+
required=True,
|
|
1324
|
+
help="Execution ID(s) to cancel (can be specified multiple times)",
|
|
1325
|
+
)
|
|
1326
|
+
def cancel_executions(execution_ids: tuple) -> None:
|
|
1327
|
+
"""Cancel one or more function executions."""
|
|
1328
|
+
url = f"{get_unified_v2_base()}/executions/cancel"
|
|
1329
|
+
payload = {"ids": list(execution_ids)}
|
|
1330
|
+
try:
|
|
1331
|
+
resp = make_api_request("POST", url, payload, handle_errors=False)
|
|
1332
|
+
if resp.status_code == 204:
|
|
1333
|
+
if len(execution_ids) == 1:
|
|
1334
|
+
click.echo(f"✓ Execution {execution_ids[0]} cancelled successfully.")
|
|
1335
|
+
else:
|
|
1336
|
+
click.echo(f"✓ All {len(execution_ids)} executions cancelled successfully.")
|
|
1337
|
+
return
|
|
1338
|
+
if resp.status_code == 200:
|
|
1339
|
+
data = resp.json()
|
|
1340
|
+
cancelled = data.get("cancelled", [])
|
|
1341
|
+
failed = data.get("failed", [])
|
|
1342
|
+
if cancelled:
|
|
1343
|
+
if len(cancelled) == 1:
|
|
1344
|
+
click.echo(f"✓ Execution {cancelled[0]} cancelled successfully.")
|
|
1345
|
+
else:
|
|
1346
|
+
click.echo(f"✓ {len(cancelled)} executions cancelled successfully:")
|
|
1347
|
+
for eid in cancelled:
|
|
1348
|
+
click.echo(f" - {eid}")
|
|
1349
|
+
if failed:
|
|
1350
|
+
click.echo(f"✗ Failed to cancel {len(failed)} execution(s):", err=True)
|
|
1351
|
+
for failure in failed:
|
|
1352
|
+
eid = failure.get("id", "unknown")
|
|
1353
|
+
err_msg = failure.get("error", {}).get("message", "Unknown error")
|
|
1354
|
+
click.echo(f" - {eid}: {err_msg}", err=True)
|
|
1355
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
1356
|
+
return
|
|
1357
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
1358
|
+
display_api_errors(
|
|
1359
|
+
"Function execution cancellation failed", response_data, detailed=True
|
|
1360
|
+
)
|
|
1361
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
1362
|
+
except Exception as exc: # noqa: BLE001
|
|
1363
|
+
handle_api_error(exc)
|
|
1364
|
+
|
|
1365
|
+
@execute_group.command(name="retry")
|
|
1366
|
+
@click.option(
|
|
1367
|
+
"--id",
|
|
1368
|
+
"-i",
|
|
1369
|
+
"execution_ids",
|
|
1370
|
+
multiple=True,
|
|
1371
|
+
required=True,
|
|
1372
|
+
help="Execution ID(s) to retry (can be specified multiple times)",
|
|
1373
|
+
)
|
|
1374
|
+
def retry_executions(execution_ids: tuple) -> None:
|
|
1375
|
+
"""Retry one or more failed function executions."""
|
|
1376
|
+
url = f"{get_unified_v2_base()}/executions/retry"
|
|
1377
|
+
payload = {"ids": list(execution_ids)}
|
|
1378
|
+
try:
|
|
1379
|
+
resp = make_api_request("POST", url, payload, handle_errors=False)
|
|
1380
|
+
if resp.status_code in (200, 201):
|
|
1381
|
+
data = resp.json()
|
|
1382
|
+
executions = data.get("executions", [])
|
|
1383
|
+
failed = data.get("failed", [])
|
|
1384
|
+
if executions:
|
|
1385
|
+
click.echo(f"✓ {len(executions)} retry executions created successfully:")
|
|
1386
|
+
for execution in executions:
|
|
1387
|
+
click.echo(f" - New execution: {execution.get('id', '')}")
|
|
1388
|
+
if failed:
|
|
1389
|
+
click.echo(f"✗ Failed to retry {len(failed)} execution(s):", err=True)
|
|
1390
|
+
for failure in failed:
|
|
1391
|
+
eid = failure.get("id", "unknown")
|
|
1392
|
+
err_msg = failure.get("error", {}).get("message", "Unknown error")
|
|
1393
|
+
click.echo(f" - {eid}: {err_msg}", err=True)
|
|
1394
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
1395
|
+
return
|
|
1396
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
1397
|
+
display_api_errors("Function execution retry failed", response_data, detailed=True)
|
|
1398
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
1399
|
+
except Exception as exc: # noqa: BLE001
|
|
1400
|
+
handle_api_error(exc)
|