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.
Files changed (49) hide show
  1. airpuls_sdk_cli/__init__.py +39 -0
  2. airpuls_sdk_cli/__main__.py +5 -0
  3. airpuls_sdk_cli/app.py +334 -0
  4. airpuls_sdk_cli/commands/__init__.py +7 -0
  5. airpuls_sdk_cli/commands/config.py +390 -0
  6. airpuls_sdk_cli/commands/doctor.py +82 -0
  7. airpuls_sdk_cli/commands/xapp_add_sink.py +148 -0
  8. airpuls_sdk_cli/commands/xapp_build.py +42 -0
  9. airpuls_sdk_cli/commands/xapp_connect.py +127 -0
  10. airpuls_sdk_cli/commands/xapp_credentials.py +169 -0
  11. airpuls_sdk_cli/commands/xapp_deploy.py +315 -0
  12. airpuls_sdk_cli/commands/xapp_info.py +48 -0
  13. airpuls_sdk_cli/commands/xapp_new.py +208 -0
  14. airpuls_sdk_cli/commands/xapp_package.py +48 -0
  15. airpuls_sdk_cli/commands/xapp_run.py +226 -0
  16. airpuls_sdk_cli/commands/xapp_test.py +54 -0
  17. airpuls_sdk_cli/commands/xapp_undeploy.py +183 -0
  18. airpuls_sdk_cli/commands/xapp_validate.py +45 -0
  19. airpuls_sdk_cli/core/__init__.py +6 -0
  20. airpuls_sdk_cli/core/console.py +199 -0
  21. airpuls_sdk_cli/core/deploy.py +659 -0
  22. airpuls_sdk_cli/core/emit.py +219 -0
  23. airpuls_sdk_cli/core/errors.py +115 -0
  24. airpuls_sdk_cli/core/manifest.py +343 -0
  25. airpuls_sdk_cli/core/runner.py +262 -0
  26. airpuls_sdk_cli/core/scaffold.py +366 -0
  27. airpuls_sdk_cli/core/settings.py +289 -0
  28. airpuls_sdk_cli/core/sinks.py +237 -0
  29. airpuls_sdk_cli/core/sshauth.py +262 -0
  30. airpuls_sdk_cli/core/targetselect.py +149 -0
  31. airpuls_sdk_cli/core/toolchain.py +451 -0
  32. airpuls_sdk_cli/core/wizard.py +337 -0
  33. airpuls_sdk_cli/core/workspace.py +162 -0
  34. airpuls_sdk_cli/templates/xapp/README.md.j2 +140 -0
  35. airpuls_sdk_cli/templates/xapp/__name__.xapp.j2 +59 -0
  36. airpuls_sdk_cli/templates/xapp/dot-airpuls-xapp.yml.j2 +15 -0
  37. airpuls_sdk_cli/templates/xapp/dot-devcontainer/Dockerfile.j2 +28 -0
  38. airpuls_sdk_cli/templates/xapp/dot-devcontainer/devcontainer.json.j2 +41 -0
  39. airpuls_sdk_cli/templates/xapp/dot-gitignore.j2 +29 -0
  40. airpuls_sdk_cli/templates/xapp/python/Dockerfile.j2 +15 -0
  41. airpuls_sdk_cli/templates/xapp/python/main.py.j2 +210 -0
  42. airpuls_sdk_cli/templates/xapp/tests/test_config.py.j2 +23 -0
  43. airpuls_sdk_cli/templates/xapp/xapp.schema.json.j2 +522 -0
  44. airpuls_sdk_cli/templates/xapp/xapp.yml.j2 +64 -0
  45. airpuls_sdk_cli-0.1.4.dist-info/METADATA +251 -0
  46. airpuls_sdk_cli-0.1.4.dist-info/RECORD +49 -0
  47. airpuls_sdk_cli-0.1.4.dist-info/WHEEL +5 -0
  48. airpuls_sdk_cli-0.1.4.dist-info/entry_points.txt +2 -0
  49. 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})"
@@ -0,0 +1,5 @@
1
+ """Module execution entry: ``python -m airpuls_sdk_cli``."""
2
+
3
+ from .app import main
4
+
5
+ main()
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."""