airpuls-sdk-cli 0.1.4__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.
- airpuls_sdk_cli/__init__.py +39 -0
- airpuls_sdk_cli/__main__.py +5 -0
- airpuls_sdk_cli/app.py +334 -0
- airpuls_sdk_cli/commands/__init__.py +7 -0
- airpuls_sdk_cli/commands/config.py +390 -0
- airpuls_sdk_cli/commands/doctor.py +82 -0
- airpuls_sdk_cli/commands/xapp_add_sink.py +148 -0
- airpuls_sdk_cli/commands/xapp_build.py +42 -0
- airpuls_sdk_cli/commands/xapp_connect.py +127 -0
- airpuls_sdk_cli/commands/xapp_credentials.py +169 -0
- airpuls_sdk_cli/commands/xapp_deploy.py +315 -0
- airpuls_sdk_cli/commands/xapp_info.py +48 -0
- airpuls_sdk_cli/commands/xapp_new.py +208 -0
- airpuls_sdk_cli/commands/xapp_package.py +48 -0
- airpuls_sdk_cli/commands/xapp_run.py +226 -0
- airpuls_sdk_cli/commands/xapp_test.py +54 -0
- airpuls_sdk_cli/commands/xapp_undeploy.py +183 -0
- airpuls_sdk_cli/commands/xapp_validate.py +45 -0
- airpuls_sdk_cli/core/__init__.py +6 -0
- airpuls_sdk_cli/core/console.py +199 -0
- airpuls_sdk_cli/core/deploy.py +659 -0
- airpuls_sdk_cli/core/emit.py +219 -0
- airpuls_sdk_cli/core/errors.py +115 -0
- airpuls_sdk_cli/core/manifest.py +343 -0
- airpuls_sdk_cli/core/runner.py +262 -0
- airpuls_sdk_cli/core/scaffold.py +366 -0
- airpuls_sdk_cli/core/settings.py +289 -0
- airpuls_sdk_cli/core/sinks.py +237 -0
- airpuls_sdk_cli/core/sshauth.py +262 -0
- airpuls_sdk_cli/core/targetselect.py +149 -0
- airpuls_sdk_cli/core/toolchain.py +451 -0
- airpuls_sdk_cli/core/wizard.py +337 -0
- airpuls_sdk_cli/core/workspace.py +162 -0
- airpuls_sdk_cli/templates/xapp/README.md.j2 +140 -0
- airpuls_sdk_cli/templates/xapp/__name__.xapp.j2 +59 -0
- airpuls_sdk_cli/templates/xapp/dot-airpuls-xapp.yml.j2 +15 -0
- airpuls_sdk_cli/templates/xapp/dot-devcontainer/Dockerfile.j2 +28 -0
- airpuls_sdk_cli/templates/xapp/dot-devcontainer/devcontainer.json.j2 +41 -0
- airpuls_sdk_cli/templates/xapp/dot-gitignore.j2 +29 -0
- airpuls_sdk_cli/templates/xapp/python/Dockerfile.j2 +15 -0
- airpuls_sdk_cli/templates/xapp/python/main.py.j2 +210 -0
- airpuls_sdk_cli/templates/xapp/tests/test_config.py.j2 +23 -0
- airpuls_sdk_cli/templates/xapp/xapp.schema.json.j2 +522 -0
- airpuls_sdk_cli/templates/xapp/xapp.yml.j2 +64 -0
- airpuls_sdk_cli-0.1.4.dist-info/METADATA +251 -0
- airpuls_sdk_cli-0.1.4.dist-info/RECORD +49 -0
- airpuls_sdk_cli-0.1.4.dist-info/WHEEL +5 -0
- airpuls_sdk_cli-0.1.4.dist-info/entry_points.txt +2 -0
- airpuls_sdk_cli-0.1.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""airpuls-sdk — command-line tool for xApp project scaffolding and lifecycle.
|
|
2
|
+
|
|
3
|
+
Published as the pure-Python ``airpuls-sdk-cli`` distribution, separate
|
|
4
|
+
from the ``airpuls-ric-sdk`` xApp runtime: the tool never imports the
|
|
5
|
+
runtime, so it installs and works on every platform a developer works
|
|
6
|
+
from, while the runtime ships as a Linux x86_64 binary wheel. Commands
|
|
7
|
+
that execute an xApp need the runtime in the environment they launch;
|
|
8
|
+
``doctor`` reports whether it is importable, and ``xapp run`` explains
|
|
9
|
+
its absence.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from importlib import metadata
|
|
15
|
+
|
|
16
|
+
DISTRIBUTION_NAME = "airpuls-sdk-cli"
|
|
17
|
+
|
|
18
|
+
RELEASE_STAGE = "beta"
|
|
19
|
+
|
|
20
|
+
_FALLBACK_VERSION = "0.0.0.dev0"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def distribution_version() -> str:
|
|
24
|
+
"""Return the installed ``airpuls-sdk-cli`` distribution version.
|
|
25
|
+
|
|
26
|
+
Falls back to a development placeholder when the distribution
|
|
27
|
+
metadata is unavailable, e.g. when running from a source checkout
|
|
28
|
+
that was never installed.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
return metadata.version(DISTRIBUTION_NAME)
|
|
32
|
+
except metadata.PackageNotFoundError:
|
|
33
|
+
return _FALLBACK_VERSION
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cli_version() -> str:
|
|
37
|
+
"""Return the human-readable CLI version string, including the
|
|
38
|
+
release stage, e.g. ``0.1.0 (beta)``."""
|
|
39
|
+
return f"{distribution_version()} ({RELEASE_STAGE})"
|
airpuls_sdk_cli/app.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Root application of the airpuls-sdk CLI.
|
|
2
|
+
|
|
3
|
+
Assembles the command tree and owns the single failure-rendering
|
|
4
|
+
guard: commands raise :class:`~.core.errors.CliError`, and
|
|
5
|
+
:func:`main` maps every outcome — success, typed failure, usage
|
|
6
|
+
error, interrupt, unexpected exception — onto exactly one rendering
|
|
7
|
+
and exit status.
|
|
8
|
+
|
|
9
|
+
The same guard selects the output mode. With ``--json`` every
|
|
10
|
+
outcome is written as one document on stdout by :mod:`.core.emit`
|
|
11
|
+
instead of being rendered, which is the interface editor integrations
|
|
12
|
+
consume; exit statuses are identical in both modes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from typing import Annotated, Optional, Sequence
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from typer import _click as clicklib
|
|
24
|
+
except ImportError:
|
|
25
|
+
import click as clicklib
|
|
26
|
+
|
|
27
|
+
from . import RELEASE_STAGE, cli_version, distribution_version
|
|
28
|
+
from .commands import (
|
|
29
|
+
config,
|
|
30
|
+
doctor,
|
|
31
|
+
xapp_add_sink,
|
|
32
|
+
xapp_build,
|
|
33
|
+
xapp_connect,
|
|
34
|
+
xapp_credentials,
|
|
35
|
+
xapp_deploy,
|
|
36
|
+
xapp_info,
|
|
37
|
+
xapp_new,
|
|
38
|
+
xapp_package,
|
|
39
|
+
xapp_run,
|
|
40
|
+
xapp_test,
|
|
41
|
+
xapp_undeploy,
|
|
42
|
+
xapp_validate,
|
|
43
|
+
)
|
|
44
|
+
from .core import console as console_mod
|
|
45
|
+
from .core import emit
|
|
46
|
+
from .core.console import render_error, render_unexpected
|
|
47
|
+
from .core.errors import CliError, UsageError
|
|
48
|
+
|
|
49
|
+
app = typer.Typer(
|
|
50
|
+
name="airpuls-sdk",
|
|
51
|
+
help="Developer tool for airpuls Near-RT RIC xApps (beta).",
|
|
52
|
+
no_args_is_help=True,
|
|
53
|
+
add_completion=True,
|
|
54
|
+
rich_markup_mode="rich",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
xapp_app = typer.Typer(
|
|
58
|
+
help="Create and manage xApp projects.", no_args_is_help=True
|
|
59
|
+
)
|
|
60
|
+
app.add_typer(xapp_app, name="xapp")
|
|
61
|
+
|
|
62
|
+
config_app = typer.Typer(
|
|
63
|
+
help="Store and inspect CLI settings.", no_args_is_help=True
|
|
64
|
+
)
|
|
65
|
+
app.add_typer(config_app, name="config")
|
|
66
|
+
|
|
67
|
+
xapp_app.command("new")(xapp_new.new)
|
|
68
|
+
xapp_app.command("validate")(xapp_validate.validate)
|
|
69
|
+
xapp_app.command("info")(xapp_info.info)
|
|
70
|
+
xapp_app.command("build")(xapp_build.build)
|
|
71
|
+
xapp_app.command("test")(xapp_test.test)
|
|
72
|
+
xapp_app.command("run")(xapp_run.run)
|
|
73
|
+
xapp_app.command("package")(xapp_package.package)
|
|
74
|
+
xapp_app.command("deploy")(xapp_deploy.deploy_cmd)
|
|
75
|
+
xapp_app.command("undeploy")(xapp_undeploy.undeploy)
|
|
76
|
+
xapp_app.command("credentials")(xapp_credentials.credentials)
|
|
77
|
+
xapp_app.command("connect")(xapp_connect.connect)
|
|
78
|
+
add_app = typer.Typer(
|
|
79
|
+
help="Add optional configuration to the project.", no_args_is_help=True
|
|
80
|
+
)
|
|
81
|
+
xapp_app.add_typer(add_app, name="add")
|
|
82
|
+
add_app.command("sink")(xapp_add_sink.add_sink)
|
|
83
|
+
ric_app = typer.Typer(
|
|
84
|
+
help="Manage the registry of RIC deployment targets.",
|
|
85
|
+
no_args_is_help=True,
|
|
86
|
+
)
|
|
87
|
+
config_app.add_typer(ric_app, name="ric")
|
|
88
|
+
ric_app.command("add")(config.ric_add)
|
|
89
|
+
ric_app.command("list")(config.ric_list)
|
|
90
|
+
ric_app.command("remove")(config.ric_remove)
|
|
91
|
+
ric_app.command("use")(config.ric_use)
|
|
92
|
+
ric_app.command("probe")(config.ric_probe)
|
|
93
|
+
ric_app.command("logs")(config.ric_logs)
|
|
94
|
+
config_app.command("show")(config.show)
|
|
95
|
+
app.command("doctor")(doctor.doctor)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _print_version(value: bool) -> None:
|
|
99
|
+
"""Eager ``--version`` callback: report the version and stop.
|
|
100
|
+
|
|
101
|
+
:raises typer.Exit: Always, after reporting, when ``value`` is set.
|
|
102
|
+
"""
|
|
103
|
+
if value:
|
|
104
|
+
emit.data(version=distribution_version(), stage=RELEASE_STAGE)
|
|
105
|
+
console_mod.console.print(f"airpuls-sdk {cli_version()}")
|
|
106
|
+
raise typer.Exit()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@app.callback()
|
|
110
|
+
def _root(
|
|
111
|
+
version: Annotated[
|
|
112
|
+
Optional[bool],
|
|
113
|
+
typer.Option(
|
|
114
|
+
"--version",
|
|
115
|
+
callback=_print_version,
|
|
116
|
+
is_eager=True,
|
|
117
|
+
help="Print the CLI version and exit.",
|
|
118
|
+
),
|
|
119
|
+
] = None,
|
|
120
|
+
debug: Annotated[
|
|
121
|
+
bool,
|
|
122
|
+
typer.Option(
|
|
123
|
+
"--debug",
|
|
124
|
+
help="Show full tracebacks for unexpected errors.",
|
|
125
|
+
),
|
|
126
|
+
] = False,
|
|
127
|
+
json_output: Annotated[
|
|
128
|
+
bool,
|
|
129
|
+
typer.Option(
|
|
130
|
+
"--json",
|
|
131
|
+
envvar=emit.ENV_VAR,
|
|
132
|
+
help="Report the result as one JSON document on stdout "
|
|
133
|
+
"instead of rendered text, and never prompt. Intended for "
|
|
134
|
+
"editor integrations and scripts.",
|
|
135
|
+
),
|
|
136
|
+
] = False,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Record process-wide presentation flags before any command runs.
|
|
139
|
+
|
|
140
|
+
The JSON channel is already active at this point — :func:`main`
|
|
141
|
+
resolves it from the raw arguments — so the flag is declared here
|
|
142
|
+
for parsing and help only.
|
|
143
|
+
"""
|
|
144
|
+
console_mod.state["debug"] = debug
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
INTERRUPT_EXIT_CODE = 130
|
|
148
|
+
"""Exit status of an interrupted command, the shell's SIGINT
|
|
149
|
+
convention. Reported both as a raised abort and, depending on the
|
|
150
|
+
click generation underneath Typer, as the application call's return
|
|
151
|
+
value."""
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
HELP_FLAG = "--help"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _resolve_command(argv: Sequence[str]):
|
|
158
|
+
"""Resolve ``argv`` against the command tree.
|
|
159
|
+
|
|
160
|
+
Walks the leading non-option tokens as long as each names a
|
|
161
|
+
subcommand, which identifies the invoked command without
|
|
162
|
+
interpreting its arguments.
|
|
163
|
+
|
|
164
|
+
:returns: (click command object, its path components).
|
|
165
|
+
"""
|
|
166
|
+
command = typer.main.get_command(app)
|
|
167
|
+
parts: list[str] = []
|
|
168
|
+
for token in argv:
|
|
169
|
+
if token.startswith("-"):
|
|
170
|
+
continue
|
|
171
|
+
children = getattr(command, "commands", {})
|
|
172
|
+
if token not in children:
|
|
173
|
+
break
|
|
174
|
+
parts.append(token)
|
|
175
|
+
command = children[token]
|
|
176
|
+
return command, parts
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _command_path(argv: Sequence[str]) -> str:
|
|
180
|
+
"""Name the invoked command as a space-separated path, e.g.
|
|
181
|
+
``xapp deploy``; empty when no command was named."""
|
|
182
|
+
return " ".join(_resolve_command(argv)[1])
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _parameter_record(param) -> dict:
|
|
186
|
+
"""Describe one command parameter as document content."""
|
|
187
|
+
return {
|
|
188
|
+
"names": list(param.opts),
|
|
189
|
+
"help": getattr(param, "help", None) or "",
|
|
190
|
+
"required": bool(param.required),
|
|
191
|
+
"is_flag": bool(getattr(param, "is_flag", False)),
|
|
192
|
+
"default": emit.plain(param.default),
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _help_record(command) -> dict:
|
|
197
|
+
"""Describe ``command`` as document content: its own help, the
|
|
198
|
+
subcommands it groups, and the parameters it accepts.
|
|
199
|
+
|
|
200
|
+
Replaces the rendered help in JSON mode, where an integration
|
|
201
|
+
needs the command surface as data to build its own affordances.
|
|
202
|
+
|
|
203
|
+
:returns: The record; caller owns it.
|
|
204
|
+
"""
|
|
205
|
+
subcommands = getattr(command, "commands", {})
|
|
206
|
+
return {
|
|
207
|
+
"help": (command.help or "").strip(),
|
|
208
|
+
"commands": [
|
|
209
|
+
{"name": name, "help": sub.get_short_help_str(limit=200)}
|
|
210
|
+
for name, sub in sorted(subcommands.items())
|
|
211
|
+
],
|
|
212
|
+
"arguments": [
|
|
213
|
+
_parameter_record(param)
|
|
214
|
+
for param in command.params
|
|
215
|
+
if param.param_type_name == "argument"
|
|
216
|
+
],
|
|
217
|
+
"options": [
|
|
218
|
+
_parameter_record(param)
|
|
219
|
+
for param in command.params
|
|
220
|
+
if param.param_type_name == "option"
|
|
221
|
+
],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _record_interrupt() -> None:
|
|
226
|
+
"""Record the interrupt as the invocation's failure."""
|
|
227
|
+
emit.failure(
|
|
228
|
+
"interrupted",
|
|
229
|
+
"command interrupted",
|
|
230
|
+
exit_code=INTERRUPT_EXIT_CODE,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _finish(command: str, exit_code: int) -> SystemExit:
|
|
235
|
+
"""Close the invocation in the active output mode.
|
|
236
|
+
|
|
237
|
+
Writes the result document when the machine-readable channel is
|
|
238
|
+
on; text mode has already reported everything at its call sites.
|
|
239
|
+
|
|
240
|
+
:returns: The exit the caller raises.
|
|
241
|
+
"""
|
|
242
|
+
if emit.enabled():
|
|
243
|
+
emit.write(emit.document(command, exit_code=exit_code))
|
|
244
|
+
return SystemExit(exit_code)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _fail(command: str, err: CliError) -> SystemExit:
|
|
248
|
+
"""Report a typed failure as a rendered panel or as the result
|
|
249
|
+
document's ``error`` block.
|
|
250
|
+
|
|
251
|
+
:returns: The exit carrying the failure's status.
|
|
252
|
+
"""
|
|
253
|
+
if emit.enabled():
|
|
254
|
+
emit.failure_from(err)
|
|
255
|
+
else:
|
|
256
|
+
render_error(err)
|
|
257
|
+
return _finish(command, err.exit_code)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _usage_error(exc: clicklib.ClickException) -> UsageError:
|
|
261
|
+
"""Map a command-line parsing failure onto the CLI's usage error,
|
|
262
|
+
naming the placement rule when the JSON flag was rejected because
|
|
263
|
+
it followed the command."""
|
|
264
|
+
message = exc.format_message()
|
|
265
|
+
if emit.FLAG in message:
|
|
266
|
+
return UsageError(
|
|
267
|
+
message,
|
|
268
|
+
hint=f"pass {emit.FLAG} before the command: "
|
|
269
|
+
f"`airpuls-sdk {emit.FLAG} <command>`",
|
|
270
|
+
)
|
|
271
|
+
return UsageError(message, hint="run `airpuls-sdk --help` for usage")
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def main(argv: Optional[Sequence[str]] = None) -> None:
|
|
275
|
+
"""Console-script entry point.
|
|
276
|
+
|
|
277
|
+
Resolves the output mode from the raw arguments, then runs the
|
|
278
|
+
Typer application in non-standalone mode so every failure path
|
|
279
|
+
funnels through the CLI's own reporting. Depending on the
|
|
280
|
+
underlying click generation, a ``typer.Exit`` and an interrupt
|
|
281
|
+
surface either as the return value of the application call or as a
|
|
282
|
+
raised exception; both forms are mapped to the same exit status
|
|
283
|
+
and, in JSON mode, to the same document.
|
|
284
|
+
|
|
285
|
+
:param argv: Argument vector override; None uses ``sys.argv[1:]``.
|
|
286
|
+
:raises SystemExit: Always, carrying the mapped exit status —
|
|
287
|
+
0 success, :class:`CliError` codes 1–5,
|
|
288
|
+
2 for command-line usage errors, 130 for
|
|
289
|
+
interrupts.
|
|
290
|
+
"""
|
|
291
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
292
|
+
emit.reset()
|
|
293
|
+
emit.configure(argv)
|
|
294
|
+
console_mod.set_quiet(emit.enabled())
|
|
295
|
+
resolved, parts = _resolve_command(argv)
|
|
296
|
+
command = " ".join(parts)
|
|
297
|
+
|
|
298
|
+
if emit.enabled() and HELP_FLAG in argv:
|
|
299
|
+
emit.data(**_help_record(resolved))
|
|
300
|
+
raise _finish(command, 0)
|
|
301
|
+
|
|
302
|
+
outcome = 0
|
|
303
|
+
try:
|
|
304
|
+
outcome = app(args=argv, prog_name="airpuls-sdk", standalone_mode=False)
|
|
305
|
+
except CliError as err:
|
|
306
|
+
raise _fail(command, err) from None
|
|
307
|
+
except clicklib.exceptions.Exit as exc:
|
|
308
|
+
raise _finish(command, exc.exit_code) from None
|
|
309
|
+
except (clicklib.exceptions.Abort, KeyboardInterrupt):
|
|
310
|
+
_record_interrupt()
|
|
311
|
+
raise _finish(command, INTERRUPT_EXIT_CODE) from None
|
|
312
|
+
except clicklib.ClickException as exc:
|
|
313
|
+
if not exc.format_message().strip():
|
|
314
|
+
raise _finish(command, exc.exit_code) from None
|
|
315
|
+
raise _fail(command, _usage_error(exc)) from None
|
|
316
|
+
except Exception as exc: # noqa: BLE001 — final guard reports and exits
|
|
317
|
+
if emit.enabled():
|
|
318
|
+
emit.failure(
|
|
319
|
+
"internal",
|
|
320
|
+
f"{type(exc).__name__}: {exc}",
|
|
321
|
+
exit_code=1,
|
|
322
|
+
hint="re-run with --debug for the full traceback",
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
render_unexpected(exc)
|
|
326
|
+
raise _finish(command, 1) from None
|
|
327
|
+
status = outcome if isinstance(outcome, int) else 0
|
|
328
|
+
if status == INTERRUPT_EXIT_CODE:
|
|
329
|
+
_record_interrupt()
|
|
330
|
+
raise _finish(command, status)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if __name__ == "__main__":
|
|
334
|
+
main()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Command layer of the airpuls-sdk CLI.
|
|
2
|
+
|
|
3
|
+
One module per command; each exposes a single Typer-compatible
|
|
4
|
+
function that parses input, delegates to ``airpuls_sdk_cli.core``,
|
|
5
|
+
and renders the outcome. Failures propagate as
|
|
6
|
+
:class:`~airpuls_sdk_cli.core.errors.CliError` to the top-level guard
|
|
7
|
+
in ``app.main`` — command modules never print errors themselves."""
|