netbox-super-cli 1.0.0__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.
- netbox_super_cli-1.0.0.dist-info/METADATA +182 -0
- netbox_super_cli-1.0.0.dist-info/RECORD +71 -0
- netbox_super_cli-1.0.0.dist-info/WHEEL +4 -0
- netbox_super_cli-1.0.0.dist-info/entry_points.txt +3 -0
- netbox_super_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
- nsc/__init__.py +5 -0
- nsc/__main__.py +6 -0
- nsc/_version.py +3 -0
- nsc/aliases/__init__.py +24 -0
- nsc/aliases/resolver.py +112 -0
- nsc/auth/__init__.py +5 -0
- nsc/auth/verify.py +143 -0
- nsc/builder/__init__.py +5 -0
- nsc/builder/build.py +514 -0
- nsc/cache/__init__.py +5 -0
- nsc/cache/store.py +295 -0
- nsc/cli/__init__.py +1 -0
- nsc/cli/aliases_commands.py +264 -0
- nsc/cli/app.py +291 -0
- nsc/cli/cache_commands.py +159 -0
- nsc/cli/commands_dump.py +57 -0
- nsc/cli/config_commands.py +156 -0
- nsc/cli/globals.py +65 -0
- nsc/cli/handlers.py +660 -0
- nsc/cli/init_commands.py +95 -0
- nsc/cli/login_commands.py +265 -0
- nsc/cli/profiles_commands.py +256 -0
- nsc/cli/registration.py +465 -0
- nsc/cli/runtime.py +290 -0
- nsc/cli/skill_commands.py +186 -0
- nsc/cli/writes/__init__.py +10 -0
- nsc/cli/writes/apply.py +177 -0
- nsc/cli/writes/bulk.py +231 -0
- nsc/cli/writes/coercion.py +9 -0
- nsc/cli/writes/confirmation.py +96 -0
- nsc/cli/writes/input.py +358 -0
- nsc/cli/writes/preflight.py +182 -0
- nsc/config/__init__.py +23 -0
- nsc/config/loader.py +69 -0
- nsc/config/models.py +54 -0
- nsc/config/settings.py +36 -0
- nsc/config/writer.py +207 -0
- nsc/http/__init__.py +6 -0
- nsc/http/audit.py +183 -0
- nsc/http/client.py +365 -0
- nsc/http/errors.py +35 -0
- nsc/http/retry.py +90 -0
- nsc/model/__init__.py +23 -0
- nsc/model/command_model.py +125 -0
- nsc/output/__init__.py +1 -0
- nsc/output/csv_.py +34 -0
- nsc/output/errors.py +346 -0
- nsc/output/explain.py +194 -0
- nsc/output/flatten.py +25 -0
- nsc/output/headers.py +9 -0
- nsc/output/json_.py +21 -0
- nsc/output/jsonl.py +21 -0
- nsc/output/render.py +47 -0
- nsc/output/table.py +50 -0
- nsc/output/yaml_.py +28 -0
- nsc/schema/__init__.py +1 -0
- nsc/schema/hashing.py +24 -0
- nsc/schema/loader.py +66 -0
- nsc/schema/models.py +109 -0
- nsc/schema/source.py +120 -0
- nsc/schemas/__init__.py +1 -0
- nsc/schemas/bundled/__init__.py +1 -0
- nsc/schemas/bundled/manifest.yaml +5 -0
- nsc/schemas/bundled/netbox-4.6.0-beta2.json.gz +0 -0
- nsc/skill/__init__.py +35 -0
- skills/netbox-super-cli/SKILL.md +127 -0
nsc/cli/registration.py
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""Walk a CommandModel and register Typer commands for read AND write operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from click import Choice
|
|
11
|
+
|
|
12
|
+
from nsc.cli.handlers import (
|
|
13
|
+
handle_create,
|
|
14
|
+
handle_custom_action,
|
|
15
|
+
handle_custom_action_write,
|
|
16
|
+
handle_delete,
|
|
17
|
+
handle_get,
|
|
18
|
+
handle_list,
|
|
19
|
+
handle_update,
|
|
20
|
+
)
|
|
21
|
+
from nsc.cli.runtime import RuntimeContext
|
|
22
|
+
from nsc.config.models import OutputFormat
|
|
23
|
+
from nsc.http.errors import NetBoxAPIError, NetBoxClientError
|
|
24
|
+
from nsc.model.command_model import (
|
|
25
|
+
CommandModel,
|
|
26
|
+
HttpMethod,
|
|
27
|
+
Operation,
|
|
28
|
+
Parameter,
|
|
29
|
+
ParameterLocation,
|
|
30
|
+
PrimitiveType,
|
|
31
|
+
Resource,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
CtxFactory = Callable[[], RuntimeContext]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def register_dynamic_commands(app: typer.Typer, model: CommandModel, get_ctx: CtxFactory) -> None:
|
|
38
|
+
for tag_name, tag in sorted(model.tags.items()):
|
|
39
|
+
tag_app = typer.Typer(no_args_is_help=True, help=tag.description or "")
|
|
40
|
+
app.add_typer(tag_app, name=tag_name)
|
|
41
|
+
for resource_name, resource in sorted(tag.resources.items()):
|
|
42
|
+
resource_app = typer.Typer(no_args_is_help=True)
|
|
43
|
+
tag_app.add_typer(resource_app, name=resource_name)
|
|
44
|
+
_register_resource_commands(resource_app, tag_name, resource_name, resource, get_ctx)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _register_resource_commands(
|
|
48
|
+
app: typer.Typer,
|
|
49
|
+
tag_name: str,
|
|
50
|
+
resource_name: str,
|
|
51
|
+
resource: Resource,
|
|
52
|
+
get_ctx: CtxFactory,
|
|
53
|
+
) -> None:
|
|
54
|
+
if resource.list_op is not None:
|
|
55
|
+
_register_read(app, "list", resource.list_op, tag_name, resource_name, get_ctx, handle_list)
|
|
56
|
+
if resource.get_op is not None:
|
|
57
|
+
_register_read(app, "get", resource.get_op, tag_name, resource_name, get_ctx, handle_get)
|
|
58
|
+
if resource.create_op is not None:
|
|
59
|
+
_register_write(
|
|
60
|
+
app, "create", resource.create_op, tag_name, resource_name, get_ctx, handle_create
|
|
61
|
+
)
|
|
62
|
+
if resource.update_op is not None:
|
|
63
|
+
_register_write(
|
|
64
|
+
app, "update", resource.update_op, tag_name, resource_name, get_ctx, handle_update
|
|
65
|
+
)
|
|
66
|
+
if resource.replace_op is not None:
|
|
67
|
+
_register_write(
|
|
68
|
+
app, "replace", resource.replace_op, tag_name, resource_name, get_ctx, handle_update
|
|
69
|
+
)
|
|
70
|
+
if resource.delete_op is not None:
|
|
71
|
+
_register_write(
|
|
72
|
+
app, "delete", resource.delete_op, tag_name, resource_name, get_ctx, handle_delete
|
|
73
|
+
)
|
|
74
|
+
for action in resource.custom_actions:
|
|
75
|
+
if action.http_method is HttpMethod.GET:
|
|
76
|
+
verb = _custom_action_verb(action.operation_id, resource_name, is_write=False)
|
|
77
|
+
_register_read(
|
|
78
|
+
app, verb, action, tag_name, resource_name, get_ctx, handle_custom_action
|
|
79
|
+
)
|
|
80
|
+
elif action.http_method in {
|
|
81
|
+
HttpMethod.POST,
|
|
82
|
+
HttpMethod.PATCH,
|
|
83
|
+
HttpMethod.PUT,
|
|
84
|
+
HttpMethod.DELETE,
|
|
85
|
+
}:
|
|
86
|
+
verb = _custom_action_verb(action.operation_id, resource_name, is_write=True)
|
|
87
|
+
_register_write(
|
|
88
|
+
app, verb, action, tag_name, resource_name, get_ctx, handle_custom_action_write
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _register_read(
|
|
93
|
+
app: typer.Typer,
|
|
94
|
+
name: str,
|
|
95
|
+
operation: Operation,
|
|
96
|
+
tag_name: str,
|
|
97
|
+
resource_name: str,
|
|
98
|
+
get_ctx: CtxFactory,
|
|
99
|
+
handler: Callable[..., None],
|
|
100
|
+
) -> None:
|
|
101
|
+
closure = _build_read_closure(operation, tag_name, resource_name, get_ctx, handler)
|
|
102
|
+
app.command(name=name, help=operation.summary or operation.description or "")(closure)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _register_write(
|
|
106
|
+
app: typer.Typer,
|
|
107
|
+
name: str,
|
|
108
|
+
operation: Operation,
|
|
109
|
+
tag_name: str,
|
|
110
|
+
resource_name: str,
|
|
111
|
+
get_ctx: CtxFactory,
|
|
112
|
+
handler: Callable[..., None],
|
|
113
|
+
) -> None:
|
|
114
|
+
closure = _build_write_closure(operation, tag_name, resource_name, get_ctx, handler)
|
|
115
|
+
app.command(name=name, help=operation.summary or operation.description or "")(closure)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
_GLOBAL_FLAG_NAMES: frozenset[str] = frozenset(
|
|
119
|
+
{"output", "compact", "columns", "limit", "all_", "filter_"}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _build_read_closure(
|
|
124
|
+
operation: Operation,
|
|
125
|
+
tag_name: str,
|
|
126
|
+
resource_name: str,
|
|
127
|
+
get_ctx: CtxFactory,
|
|
128
|
+
handler: Callable[..., None],
|
|
129
|
+
) -> Callable[..., None]:
|
|
130
|
+
sig_params: list[inspect.Parameter] = []
|
|
131
|
+
for p in operation.parameters:
|
|
132
|
+
if p.location is ParameterLocation.PATH:
|
|
133
|
+
sig_params.append(_to_positional(p))
|
|
134
|
+
elif p.location is ParameterLocation.QUERY:
|
|
135
|
+
if "__" in p.name:
|
|
136
|
+
continue
|
|
137
|
+
if p.name in _GLOBAL_FLAG_NAMES:
|
|
138
|
+
continue
|
|
139
|
+
sig_params.append(_to_typed_option(p))
|
|
140
|
+
|
|
141
|
+
sig_params.extend(_global_flag_params())
|
|
142
|
+
|
|
143
|
+
def impl(**kwargs: Any) -> None:
|
|
144
|
+
output = kwargs.pop("output", None)
|
|
145
|
+
compact = kwargs.pop("compact", False)
|
|
146
|
+
columns_csv = kwargs.pop("columns", None)
|
|
147
|
+
limit = kwargs.pop("limit", None)
|
|
148
|
+
fetch_all = kwargs.pop("all_", False)
|
|
149
|
+
filters_raw: list[str] = kwargs.pop("filter_", None) or []
|
|
150
|
+
ctx = get_ctx()
|
|
151
|
+
update: dict[str, Any] = {
|
|
152
|
+
"compact": compact,
|
|
153
|
+
"columns_override": columns_csv.split(",") if columns_csv else None,
|
|
154
|
+
"limit": limit,
|
|
155
|
+
"fetch_all": fetch_all,
|
|
156
|
+
"filters": [
|
|
157
|
+
(item.split("=", 1)[0], item.split("=", 1)[1])
|
|
158
|
+
for item in filters_raw
|
|
159
|
+
if "=" in item
|
|
160
|
+
],
|
|
161
|
+
}
|
|
162
|
+
if output:
|
|
163
|
+
update["output_format"] = OutputFormat(output)
|
|
164
|
+
ctx = ctx.model_copy(update=update)
|
|
165
|
+
try:
|
|
166
|
+
handler(operation, op_tag=tag_name, op_resource=resource_name, ctx=ctx, **kwargs)
|
|
167
|
+
except (NetBoxAPIError, NetBoxClientError) as exc:
|
|
168
|
+
typer.echo(f"Error: {exc.render_for_cli()}", err=True)
|
|
169
|
+
raise typer.Exit(1) from exc
|
|
170
|
+
|
|
171
|
+
impl.__signature__ = inspect.Signature(parameters=sig_params) # type: ignore[attr-defined]
|
|
172
|
+
impl.__name__ = operation.operation_id
|
|
173
|
+
return impl
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _build_write_closure(
|
|
177
|
+
operation: Operation,
|
|
178
|
+
tag_name: str,
|
|
179
|
+
resource_name: str,
|
|
180
|
+
get_ctx: CtxFactory,
|
|
181
|
+
handler: Callable[..., None],
|
|
182
|
+
) -> Callable[..., None]:
|
|
183
|
+
sig_params: list[inspect.Parameter] = []
|
|
184
|
+
for p in operation.parameters:
|
|
185
|
+
if p.location is ParameterLocation.PATH:
|
|
186
|
+
sig_params.append(_to_positional(p))
|
|
187
|
+
# Query parameters on writes are rare in NetBox; skip them in 3b.
|
|
188
|
+
|
|
189
|
+
sig_params.extend(_write_flag_params())
|
|
190
|
+
|
|
191
|
+
def impl(**kwargs: Any) -> None:
|
|
192
|
+
output = kwargs.pop("output", None)
|
|
193
|
+
compact = kwargs.pop("compact", False)
|
|
194
|
+
columns_csv = kwargs.pop("columns", None)
|
|
195
|
+
apply_flag: bool = kwargs.pop("apply", False)
|
|
196
|
+
explain: bool = kwargs.pop("explain", False)
|
|
197
|
+
strict: bool = kwargs.pop("strict", False)
|
|
198
|
+
file: str | None = kwargs.pop("file", None)
|
|
199
|
+
fields_raw: list[str] = list(kwargs.pop("field", None) or [])
|
|
200
|
+
format_: str | None = kwargs.pop("format_", None)
|
|
201
|
+
bulk: bool | None = kwargs.pop("bulk", None)
|
|
202
|
+
no_bulk: bool | None = kwargs.pop("no_bulk", None)
|
|
203
|
+
on_error: str = kwargs.pop("on_error", "stop")
|
|
204
|
+
ctx = get_ctx()
|
|
205
|
+
update: dict[str, Any] = {
|
|
206
|
+
"compact": compact,
|
|
207
|
+
"columns_override": columns_csv.split(",") if columns_csv else None,
|
|
208
|
+
"apply": apply_flag,
|
|
209
|
+
"explain": explain,
|
|
210
|
+
"strict": strict,
|
|
211
|
+
"file": file,
|
|
212
|
+
"fields": fields_raw,
|
|
213
|
+
"file_format": format_,
|
|
214
|
+
"bulk": bulk,
|
|
215
|
+
"no_bulk": no_bulk,
|
|
216
|
+
"on_error": on_error,
|
|
217
|
+
}
|
|
218
|
+
if output:
|
|
219
|
+
update["output_format"] = OutputFormat(output)
|
|
220
|
+
ctx = ctx.model_copy(update=update)
|
|
221
|
+
handler(operation, op_tag=tag_name, op_resource=resource_name, ctx=ctx, **kwargs)
|
|
222
|
+
|
|
223
|
+
impl.__signature__ = inspect.Signature(parameters=sig_params) # type: ignore[attr-defined]
|
|
224
|
+
impl.__name__ = operation.operation_id
|
|
225
|
+
return impl
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _to_positional(p: Parameter) -> inspect.Parameter:
|
|
229
|
+
py_type = _python_type(p)
|
|
230
|
+
return inspect.Parameter(
|
|
231
|
+
name=p.name,
|
|
232
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
233
|
+
annotation=py_type,
|
|
234
|
+
default=typer.Argument(...),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _to_typed_option(p: Parameter) -> inspect.Parameter:
|
|
239
|
+
flag_name = f"--{p.name.replace('_', '-')}"
|
|
240
|
+
py_type: Any = _python_type(p)
|
|
241
|
+
if p.enum:
|
|
242
|
+
option = typer.Option(
|
|
243
|
+
None,
|
|
244
|
+
flag_name,
|
|
245
|
+
help=p.description or "",
|
|
246
|
+
click_type=Choice(p.enum, case_sensitive=True),
|
|
247
|
+
)
|
|
248
|
+
py_type = str | None
|
|
249
|
+
elif p.primitive is PrimitiveType.BOOLEAN:
|
|
250
|
+
option = typer.Option(
|
|
251
|
+
None,
|
|
252
|
+
f"{flag_name}/--no-{p.name.replace('_', '-')}",
|
|
253
|
+
help=p.description or "",
|
|
254
|
+
)
|
|
255
|
+
py_type = bool | None
|
|
256
|
+
elif p.primitive is PrimitiveType.ARRAY:
|
|
257
|
+
option = typer.Option(None, flag_name, help=p.description or "")
|
|
258
|
+
py_type = list[str] | None
|
|
259
|
+
else:
|
|
260
|
+
option = typer.Option(None, flag_name, help=p.description or "")
|
|
261
|
+
py_type = py_type | None
|
|
262
|
+
return inspect.Parameter(
|
|
263
|
+
name=p.name,
|
|
264
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
265
|
+
annotation=py_type,
|
|
266
|
+
default=option,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _global_flag_params() -> list[inspect.Parameter]:
|
|
271
|
+
return [
|
|
272
|
+
inspect.Parameter(
|
|
273
|
+
name="output",
|
|
274
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
275
|
+
annotation=str | None,
|
|
276
|
+
default=typer.Option(None, "--output", "-o"),
|
|
277
|
+
),
|
|
278
|
+
inspect.Parameter(
|
|
279
|
+
name="compact",
|
|
280
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
281
|
+
annotation=bool,
|
|
282
|
+
default=typer.Option(False, "--compact"),
|
|
283
|
+
),
|
|
284
|
+
inspect.Parameter(
|
|
285
|
+
name="columns",
|
|
286
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
287
|
+
annotation=str | None,
|
|
288
|
+
default=typer.Option(None, "--columns"),
|
|
289
|
+
),
|
|
290
|
+
inspect.Parameter(
|
|
291
|
+
name="limit",
|
|
292
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
293
|
+
annotation=int | None,
|
|
294
|
+
default=typer.Option(None, "--limit"),
|
|
295
|
+
),
|
|
296
|
+
inspect.Parameter(
|
|
297
|
+
name="all_",
|
|
298
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
299
|
+
annotation=bool,
|
|
300
|
+
default=typer.Option(False, "--all"),
|
|
301
|
+
),
|
|
302
|
+
inspect.Parameter(
|
|
303
|
+
name="filter_",
|
|
304
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
305
|
+
annotation=list[str] | None,
|
|
306
|
+
default=typer.Option(None, "--filter"),
|
|
307
|
+
),
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _write_flag_params() -> list[inspect.Parameter]:
|
|
312
|
+
return [
|
|
313
|
+
inspect.Parameter(
|
|
314
|
+
name="output",
|
|
315
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
316
|
+
annotation=str | None,
|
|
317
|
+
default=typer.Option(None, "--output", "-o"),
|
|
318
|
+
),
|
|
319
|
+
inspect.Parameter(
|
|
320
|
+
name="compact",
|
|
321
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
322
|
+
annotation=bool,
|
|
323
|
+
default=typer.Option(False, "--compact"),
|
|
324
|
+
),
|
|
325
|
+
inspect.Parameter(
|
|
326
|
+
name="columns",
|
|
327
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
328
|
+
annotation=str | None,
|
|
329
|
+
default=typer.Option(None, "--columns"),
|
|
330
|
+
),
|
|
331
|
+
inspect.Parameter(
|
|
332
|
+
name="apply",
|
|
333
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
334
|
+
annotation=bool,
|
|
335
|
+
default=typer.Option(
|
|
336
|
+
False,
|
|
337
|
+
"--apply",
|
|
338
|
+
"-a",
|
|
339
|
+
help="Send the request. Without this, dry-run only (no wire effect).",
|
|
340
|
+
),
|
|
341
|
+
),
|
|
342
|
+
inspect.Parameter(
|
|
343
|
+
name="explain",
|
|
344
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
345
|
+
annotation=bool,
|
|
346
|
+
default=typer.Option(
|
|
347
|
+
False,
|
|
348
|
+
"--explain",
|
|
349
|
+
help="Print the resolved request and field-level provenance.",
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
inspect.Parameter(
|
|
353
|
+
name="strict",
|
|
354
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
355
|
+
annotation=bool,
|
|
356
|
+
default=typer.Option(
|
|
357
|
+
False,
|
|
358
|
+
"--strict",
|
|
359
|
+
help="On DELETE: fail with exit 9 if the object is already absent.",
|
|
360
|
+
),
|
|
361
|
+
),
|
|
362
|
+
inspect.Parameter(
|
|
363
|
+
name="file",
|
|
364
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
365
|
+
annotation=str | None,
|
|
366
|
+
default=typer.Option(
|
|
367
|
+
None,
|
|
368
|
+
"-f",
|
|
369
|
+
"--file",
|
|
370
|
+
help="Path to a yaml/yml/json file with the request body. Use `-` for stdin.",
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
inspect.Parameter(
|
|
374
|
+
name="field",
|
|
375
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
376
|
+
annotation=list[str] | None,
|
|
377
|
+
default=typer.Option(
|
|
378
|
+
None,
|
|
379
|
+
"--field",
|
|
380
|
+
help=(
|
|
381
|
+
"key=value field override; repeatable; dotted paths allowed "
|
|
382
|
+
"(site.name=us-east-1)."
|
|
383
|
+
),
|
|
384
|
+
),
|
|
385
|
+
),
|
|
386
|
+
inspect.Parameter(
|
|
387
|
+
name="format_",
|
|
388
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
389
|
+
annotation=str | None,
|
|
390
|
+
default=typer.Option(
|
|
391
|
+
None,
|
|
392
|
+
"--format",
|
|
393
|
+
help="Override file-format detection (yaml|yml|json).",
|
|
394
|
+
),
|
|
395
|
+
),
|
|
396
|
+
inspect.Parameter(
|
|
397
|
+
name="bulk",
|
|
398
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
399
|
+
annotation=bool | None,
|
|
400
|
+
default=typer.Option(
|
|
401
|
+
None,
|
|
402
|
+
"--bulk",
|
|
403
|
+
help=(
|
|
404
|
+
"Force a bulk POST/PATCH (single HTTP call with array body). "
|
|
405
|
+
"Errors if the endpoint is not bulk-capable."
|
|
406
|
+
),
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
inspect.Parameter(
|
|
410
|
+
name="no_bulk",
|
|
411
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
412
|
+
annotation=bool | None,
|
|
413
|
+
default=typer.Option(
|
|
414
|
+
None,
|
|
415
|
+
"--no-bulk",
|
|
416
|
+
help="Force per-record looping (N sequential HTTP calls).",
|
|
417
|
+
),
|
|
418
|
+
),
|
|
419
|
+
inspect.Parameter(
|
|
420
|
+
name="on_error",
|
|
421
|
+
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
422
|
+
annotation=str,
|
|
423
|
+
default=typer.Option(
|
|
424
|
+
"stop",
|
|
425
|
+
"--on-error",
|
|
426
|
+
help=(
|
|
427
|
+
"stop|continue. stop: abort on first failure (default). "
|
|
428
|
+
"continue: attempt every record; final exit code = worst error type."
|
|
429
|
+
),
|
|
430
|
+
),
|
|
431
|
+
),
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _python_type(p: Parameter) -> Any:
|
|
436
|
+
match p.primitive:
|
|
437
|
+
case PrimitiveType.INTEGER:
|
|
438
|
+
return int
|
|
439
|
+
case PrimitiveType.NUMBER:
|
|
440
|
+
return float
|
|
441
|
+
case PrimitiveType.BOOLEAN:
|
|
442
|
+
return bool
|
|
443
|
+
case _:
|
|
444
|
+
return str
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _custom_action_verb(operation_id: str, resource_name: str, *, is_write: bool) -> str:
|
|
448
|
+
"""Derive a CLI verb from a custom-action operationId.
|
|
449
|
+
|
|
450
|
+
Reads strip `_list`/`_retrieve` so list/retrieve custom-actions read
|
|
451
|
+
naturally (e.g. `available-asns list` → `available-asns`). Writes keep
|
|
452
|
+
their action suffix so PUT/PATCH/DELETE on the same base don't collide
|
|
453
|
+
in the command tree.
|
|
454
|
+
"""
|
|
455
|
+
name = operation_id
|
|
456
|
+
read_suffixes = ("_list", "_retrieve")
|
|
457
|
+
if not is_write:
|
|
458
|
+
for suffix in read_suffixes:
|
|
459
|
+
if name.endswith(suffix):
|
|
460
|
+
name = name[: -len(suffix)]
|
|
461
|
+
break
|
|
462
|
+
res_underscored = resource_name.replace("-", "_") if resource_name else ""
|
|
463
|
+
if res_underscored and res_underscored in name:
|
|
464
|
+
name = name.split(res_underscored, 1)[-1].lstrip("_")
|
|
465
|
+
return name.replace("_", "-") or operation_id
|