mreg-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.
Files changed (53) hide show
  1. mreg_cli/__about__.py +7 -0
  2. mreg_cli/__init__.py +5 -0
  3. mreg_cli/__main__.py +8 -0
  4. mreg_cli/_version.py +16 -0
  5. mreg_cli/api/__init__.py +7 -0
  6. mreg_cli/api/abstracts.py +486 -0
  7. mreg_cli/api/endpoints.py +130 -0
  8. mreg_cli/api/fields.py +104 -0
  9. mreg_cli/api/history.py +152 -0
  10. mreg_cli/api/models.py +3416 -0
  11. mreg_cli/cli.py +382 -0
  12. mreg_cli/commands/__init__.py +1 -0
  13. mreg_cli/commands/base.py +62 -0
  14. mreg_cli/commands/dhcp.py +136 -0
  15. mreg_cli/commands/group.py +326 -0
  16. mreg_cli/commands/help.py +77 -0
  17. mreg_cli/commands/host.py +54 -0
  18. mreg_cli/commands/host_submodules/__init__.py +28 -0
  19. mreg_cli/commands/host_submodules/a_aaaa.py +452 -0
  20. mreg_cli/commands/host_submodules/bacnet.py +126 -0
  21. mreg_cli/commands/host_submodules/cname.py +166 -0
  22. mreg_cli/commands/host_submodules/core.py +507 -0
  23. mreg_cli/commands/host_submodules/rr.py +973 -0
  24. mreg_cli/commands/label.py +146 -0
  25. mreg_cli/commands/logging.py +112 -0
  26. mreg_cli/commands/network.py +516 -0
  27. mreg_cli/commands/permission.py +202 -0
  28. mreg_cli/commands/policy.py +519 -0
  29. mreg_cli/commands/recording.py +59 -0
  30. mreg_cli/commands/registry.py +56 -0
  31. mreg_cli/commands/root.py +58 -0
  32. mreg_cli/commands/zone.py +288 -0
  33. mreg_cli/config.py +253 -0
  34. mreg_cli/errorbuilder.py +193 -0
  35. mreg_cli/exceptions.py +237 -0
  36. mreg_cli/help_formatter.py +38 -0
  37. mreg_cli/main.py +238 -0
  38. mreg_cli/outputmanager.py +466 -0
  39. mreg_cli/py.typed +0 -0
  40. mreg_cli/tags.txt +55 -0
  41. mreg_cli/tokenfile.py +89 -0
  42. mreg_cli/types.py +160 -0
  43. mreg_cli/utilities/__init__.py +5 -0
  44. mreg_cli/utilities/api.py +595 -0
  45. mreg_cli/utilities/shared.py +65 -0
  46. mreg_cli/utilities/validators.py +19 -0
  47. mreg_cli-1.0.0.dist-info/AUTHORS +12 -0
  48. mreg_cli-1.0.0.dist-info/LICENSE +674 -0
  49. mreg_cli-1.0.0.dist-info/METADATA +1079 -0
  50. mreg_cli-1.0.0.dist-info/RECORD +53 -0
  51. mreg_cli-1.0.0.dist-info/WHEEL +5 -0
  52. mreg_cli-1.0.0.dist-info/entry_points.txt +2 -0
  53. mreg_cli-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,193 @@
1
+ """Builders for error messages with underlined suggestions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import shlex
7
+ from abc import ABC, abstractmethod
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ ExcOrStr = Exception | str
12
+
13
+
14
+ # Re-usable function for error builders that don't need a specific implementation
15
+ def default_get_offset(self: "ErrorBuilder") -> tuple[int, int]: # noqa: ARG001
16
+ """Get the start and end index of the underlined part of the command."""
17
+ return -1, -1
18
+
19
+
20
+ class ErrorBuilder(ABC):
21
+ """Base class for building error messages with an underline and suggestion."""
22
+
23
+ # Subclasses should implement this:
24
+ SUGGESTION = ""
25
+
26
+ # Characters used for formatting the error message
27
+ CHAR_UNDERLINE = "^"
28
+ CHAR_OFFSET = " "
29
+ CHAR_SUGGESTION = "└"
30
+
31
+ def __init__(self, command: str, exc_or_str: ExcOrStr) -> None:
32
+ """Instantiate the builder with the command that caused the error."""
33
+ self.command = command
34
+ self.exc_or_str = exc_or_str
35
+
36
+ def get_underline(self, start: int, end: int) -> str:
37
+ """Get the underline part of the message.
38
+
39
+ :param int start: The start index of the underlined part of the command.
40
+ :param int end: The end index of the underlined part of the command.
41
+
42
+ :returns: The underlined part of the command.
43
+ """
44
+ return self.offset(self.CHAR_UNDERLINE * (end - start), start)
45
+
46
+ def get_suggestion(self, start: int) -> str:
47
+ """Get the suggestion part of the message.
48
+
49
+ :param int start: The start index of the underlined part of the command.
50
+ """
51
+ msg = self.SUGGESTION
52
+ if self.CHAR_SUGGESTION:
53
+ msg = f"{self.CHAR_SUGGESTION} {msg}"
54
+ return self.offset(msg, start)
55
+
56
+ def offset(self, s: str, n: int) -> str:
57
+ """Offset the string by n spaces.
58
+
59
+ :param str s: The string to offset.
60
+ :param int n: The number of spaces to offset by.
61
+
62
+ :returns: The offset string.
63
+ """
64
+ return " " * n + s
65
+
66
+ def build(self) -> str:
67
+ """Build the error message with an underline and suggestion.
68
+
69
+ :returns: The error message with an underline and suggestion.
70
+ """
71
+ start, end = self.get_offset()
72
+ lines = [
73
+ self.command,
74
+ self.get_underline(start, end),
75
+ self.get_suggestion(start),
76
+ ]
77
+ # prepend the exception message if it exists
78
+ if self.exc_or_str:
79
+ lines.insert(0, str(self.exc_or_str))
80
+ return "\n".join(lines)
81
+
82
+ @abstractmethod
83
+ def get_offset(self) -> tuple[int, int]:
84
+ """Compute the start and end index of the underlined part of the command."""
85
+ raise NotImplementedError
86
+
87
+ @abstractmethod
88
+ def can_build(self) -> bool:
89
+ """Check if the builder can build an error message for the given command."""
90
+ raise NotImplementedError
91
+
92
+
93
+ class FallbackErrorBuilder(ErrorBuilder):
94
+ """Builder for errors without a specific implementation.
95
+
96
+ Renders the error message as a single line without an underline or suggestion.
97
+ The error message is displayed as-is.
98
+ """
99
+
100
+ get_offset = default_get_offset # needed for ABC parent class
101
+
102
+ def build(self) -> str:
103
+ """Build the error message without an underline or suggestion."""
104
+ return str(self.exc_or_str)
105
+
106
+ def can_build(self) -> bool: # noqa: D102
107
+ return True
108
+
109
+
110
+ class FilterErrorBuilder(ErrorBuilder):
111
+ """Error builder for commands with a filter."""
112
+
113
+ SUGGESTION = "Consider enclosing this part in quotes."
114
+
115
+ @staticmethod
116
+ def find_word_with_char_offset(command: str, char: str) -> tuple[int, int]:
117
+ """Find the start and end index of a word containing a specific character.
118
+
119
+ Cannot fail; will always return a tuple of two integers.
120
+
121
+ :param str command: The command to search.
122
+ :param str char: The character to search for.
123
+
124
+ :returns: Tuple of two integers representing the start and end index of the word.
125
+ Returns (-1, -1) if the character is not found.
126
+ """
127
+ command = command.strip()
128
+ if not command:
129
+ return -1, -1
130
+
131
+ # Parse the command into parts
132
+ cmd = shlex.split(command, posix=False) # keep quotes and backslashes
133
+ for part in cmd:
134
+ # Skip parts that contain quotes
135
+ # NOTE: This is a very naive check, and will not work for all cases
136
+ # (e.g. escaped quotes)
137
+ if "'" in part or '"' in part:
138
+ continue
139
+
140
+ if char in part:
141
+ try:
142
+ start, end = command.index(part), command.index(part) + len(part)
143
+ if start > end or start >= len(command) or end >= len(command):
144
+ raise ValueError(f"Invalid start and end values: {start}, {end}")
145
+ except ValueError as e:
146
+ logger.error(
147
+ "Failed to get index of char '%s' in part '%s' of command %s: %s",
148
+ char,
149
+ part,
150
+ command,
151
+ e,
152
+ )
153
+ else:
154
+ return start, end
155
+ return -1, -1
156
+
157
+ def get_offset(self) -> tuple[int, int]: # noqa: D102 (missing docstring [inherit it from parent])
158
+ return self.find_word_with_char_offset(self.command, "|")
159
+
160
+ def can_build(self) -> bool: # noqa: D102 (missing docstring [inherit it from parent])
161
+ return "|" in self.command
162
+
163
+
164
+ BUILDERS: list[type[ErrorBuilder]] = [
165
+ FilterErrorBuilder,
166
+ ]
167
+
168
+
169
+ def get_builder(command: str, exc_or_str: ExcOrStr) -> ErrorBuilder:
170
+ """Get the appropriate error builder for the given command.
171
+
172
+ :param str command: The command that caused the error.
173
+ :param exc_or_str: The exception or error message.
174
+
175
+ :returns: An error builder instance.
176
+ """
177
+ for builder in BUILDERS:
178
+ b = builder(command, exc_or_str)
179
+ if b.can_build():
180
+ return b
181
+ return FallbackErrorBuilder(command, exc_or_str)
182
+
183
+
184
+ def build_error_message(command: str, exc_or_str: ExcOrStr) -> str:
185
+ """Build an error message for the given command and exception.
186
+
187
+ :param str command: The command that caused the error.
188
+ :param exc_or_str: The exception or error message.
189
+
190
+ :returns: Error message based on the command and exception.
191
+ """
192
+ builder = get_builder(command, exc_or_str)
193
+ return builder.build()
mreg_cli/exceptions.py ADDED
@@ -0,0 +1,237 @@
1
+ """Custom exceptions for mreg_cli.
2
+
3
+ Note that Cli exceptions offer a print_self() method that prints the exception
4
+ with appropriate formatting. This is useful for printing exceptions in the
5
+ context of a CLI command.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import sys
12
+ from typing import Any
13
+
14
+ from prompt_toolkit import print_formatted_text
15
+ from prompt_toolkit.formatted_text import HTML
16
+ from prompt_toolkit.formatted_text.html import html_escape
17
+
18
+
19
+ class CliExit(Exception):
20
+ """Exception used to exit the CLI."""
21
+
22
+ pass
23
+
24
+
25
+ class CliException(Exception):
26
+ """Base exception class for the CLI."""
27
+
28
+ def escape(self) -> str:
29
+ """Get an HTML-escaped string representation of the exception."""
30
+ return html_escape(str(self))
31
+
32
+ def formatted_exception(self) -> str:
33
+ """Return a formatted string representation of the exception.
34
+
35
+ :returns: Formatted string for the exception message.
36
+ """
37
+ raise NotImplementedError("This method should be implemented by subclasses")
38
+
39
+ def print_self(self):
40
+ """Print the exception with appropriate formatting."""
41
+ print_formatted_text(HTML(self.formatted_exception()), file=sys.stdout)
42
+
43
+
44
+ class CliError(CliException):
45
+ """Exception class for CLI errors.
46
+
47
+ Errors are not recoverable and stem from internal failures that
48
+ the user cannot be expected to resolve.
49
+ """
50
+
51
+ def __init__(self, *args: Any, **kwargs: Any):
52
+ """Initialize the error."""
53
+ super().__init__(*args, **kwargs)
54
+ logging.getLogger(__name__).error(str(self))
55
+ from mreg_cli.outputmanager import OutputManager
56
+
57
+ OutputManager().add_error(str(self))
58
+
59
+ def formatted_exception(self) -> str:
60
+ """Return a string formatted with HTML red tag for the error message.
61
+
62
+ :returns: Formatted error message.
63
+ """
64
+ return f"<ansired>ERROR: {self.escape()}</ansired>"
65
+
66
+
67
+ class CliWarning(CliException):
68
+ """Exception class for CLI warnings.
69
+
70
+ Warnings should be recoverable by changing the user input.
71
+ """
72
+
73
+ def __init__(self, *args: Any, **kwargs: Any):
74
+ """Initialize the warning."""
75
+ from mreg_cli.outputmanager import OutputManager
76
+
77
+ super().__init__(*args, **kwargs)
78
+ logging.getLogger(__name__).warning(str(self))
79
+ OutputManager().add_warning(str(self))
80
+
81
+ def formatted_exception(self) -> str:
82
+ """Return a string formatted with HTML italic tag for the warning message.
83
+
84
+ :returns: Formatted warning message.
85
+ """
86
+ return f"<i>{self.escape()}</i>"
87
+
88
+
89
+ class CreateError(CliError):
90
+ """Error class for failed creation."""
91
+
92
+ pass
93
+
94
+
95
+ class PatchError(CliError):
96
+ """Error class for failed patching."""
97
+
98
+ pass
99
+
100
+
101
+ class DeleteError(CliError):
102
+ """Error class for failed deletion."""
103
+
104
+ pass
105
+
106
+
107
+ class GetError(CliError):
108
+ """Error class for failed retrieval."""
109
+
110
+ pass
111
+
112
+
113
+ class InternalError(CliError):
114
+ """Error class for internal errors."""
115
+
116
+ pass
117
+
118
+
119
+ class APIError(CliError):
120
+ """Error class for API errors."""
121
+
122
+ pass
123
+
124
+
125
+ class UnexpectedDataError(APIError):
126
+ """Error class for unexpected API data."""
127
+
128
+ pass
129
+
130
+
131
+ class ValidationError(CliError):
132
+ """Error class for validation failures."""
133
+
134
+ pass
135
+
136
+
137
+ class FileError(CliError):
138
+ """Error class for file errors."""
139
+
140
+ pass
141
+
142
+
143
+ class APINotOk(CliWarning):
144
+ """Warning class for API not returning OK."""
145
+
146
+ pass
147
+
148
+
149
+ class TooManyResults(CliWarning):
150
+ """Warning class for too many results."""
151
+
152
+ pass
153
+
154
+
155
+ class NoHistoryFound(CliWarning):
156
+ """Warning class for no history found."""
157
+
158
+ pass
159
+
160
+
161
+ class EntityNotFound(CliWarning):
162
+ """Warning class for an entity that was not found."""
163
+
164
+ pass
165
+
166
+
167
+ class EntityAlreadyExists(CliWarning):
168
+ """Warning class for an entity that already exists."""
169
+
170
+ pass
171
+
172
+
173
+ class MultipleEntititesFound(CliWarning):
174
+ """Warning class for multiple entities found."""
175
+
176
+ pass
177
+
178
+
179
+ class EntityOwnershipMismatch(CliWarning):
180
+ """Warning class for an entity that already exists but owned by someone else."""
181
+
182
+ pass
183
+
184
+
185
+ class InputFailure(CliWarning):
186
+ """Warning class for input failure."""
187
+
188
+ pass
189
+
190
+
191
+ class ForceMissing(CliWarning):
192
+ """Warning class for missing force flag."""
193
+
194
+ pass
195
+
196
+
197
+ class InvalidIPAddress(CliWarning):
198
+ """Warning class for an entity that is not an IP address."""
199
+
200
+ pass
201
+
202
+
203
+ class InvalidIPv4Address(CliWarning):
204
+ """Warning class for an entity that is not an IPv4 address."""
205
+
206
+ pass
207
+
208
+
209
+ class InvalidIPv6Address(CliWarning):
210
+ """Warning class for an entity that is not an IPv6 address."""
211
+
212
+ pass
213
+
214
+
215
+ class InvalidNetwork(CliWarning):
216
+ """Warning class for an entity that is not a network."""
217
+
218
+ pass
219
+
220
+
221
+ class NetworkOverlap(CliWarning):
222
+ """Warning class for a networkthat overlaps with another network."""
223
+
224
+ pass
225
+
226
+
227
+ class LoginFailedError(CliException):
228
+ """Error class for login failure."""
229
+
230
+ def formatted_exception(self) -> str:
231
+ """Return a string formatted with 'Login failed:' prefixing the error message.
232
+
233
+ :returns: Formatted error message.
234
+ """
235
+ return f"Login failed: {self.escape()}"
236
+
237
+ pass
@@ -0,0 +1,38 @@
1
+ """A formatter for the CLI that adds extra information and improves command formatting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from typing import Any
7
+
8
+
9
+ class CustomHelpFormatter(argparse.HelpFormatter):
10
+ """Custom help formatter to add extra information and improve command formatting."""
11
+
12
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
13
+ """Initialize the formatter."""
14
+ super().__init__(*args, **kwargs)
15
+ self._max_help_position = 35 # Adjust this to control the indentation of help text
16
+
17
+ def _format_action(self, action: argparse.Action) -> str:
18
+ """Format the action with custom width.
19
+
20
+ :param action: The argparse Action being formatted.
21
+
22
+ :returns: Formatted action string.
23
+ """
24
+ # Check if this is a _SubParsersAction (used for subcommands)
25
+ if isinstance(action, argparse._SubParsersAction):
26
+ # Use a custom heading for subcommands
27
+ formatted_action = "commands:\n"
28
+
29
+ # Format each subcommand
30
+ for subcommand, parser in action.choices.items():
31
+ subcommand_help = parser.description if parser.description else ""
32
+ formatted_subcommand = f" {subcommand:<20} {subcommand_help}"
33
+ formatted_action += formatted_subcommand + "\n"
34
+
35
+ return formatted_action
36
+
37
+ # Handle non-_SubParsersAction actions with default formatting
38
+ return super()._format_action(action)
mreg_cli/main.py ADDED
@@ -0,0 +1,238 @@
1
+ """Main entry point for mreg_cli."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import getpass
7
+ import logging
8
+
9
+ from prompt_toolkit import HTML
10
+ from prompt_toolkit.shortcuts import CompleteStyle, PromptSession
11
+
12
+ import mreg_cli.utilities.api as api
13
+ from mreg_cli.__about__ import __version__
14
+ from mreg_cli.cli import cli, source
15
+ from mreg_cli.config import MregCliConfig
16
+ from mreg_cli.exceptions import LoginFailedError
17
+ from mreg_cli.outputmanager import OutputManager
18
+ from mreg_cli.types import LogLevel
19
+ from mreg_cli.utilities.api import try_token_or_login
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def main():
25
+ """Entry point for the mreg cli."""
26
+ # Read config file first, to provide defaults
27
+ config = MregCliConfig()
28
+
29
+ parser = argparse.ArgumentParser(description="The MREG cli")
30
+
31
+ parser.add_argument(
32
+ "--version",
33
+ help="Show version and exit.",
34
+ action="store_true",
35
+ )
36
+
37
+ connect_args = parser.add_argument_group("connection settings")
38
+ connect_args.add_argument(
39
+ "--url",
40
+ default=config.get_url(),
41
+ help="use mreg server at %(metavar)s (default: %(default)s)",
42
+ metavar="URL",
43
+ )
44
+
45
+ connect_args.add_argument(
46
+ "-u",
47
+ "--user",
48
+ default=config.get("user", getpass.getuser()),
49
+ help="authenticate as %(metavar)s (default: %(default)s)",
50
+ metavar="USER",
51
+ )
52
+
53
+ mreg_args = parser.add_argument_group("mreg settings")
54
+ mreg_args.add_argument(
55
+ "-d",
56
+ "--domain",
57
+ default=config.get("domain", config.get_default_domain()),
58
+ help="default %(metavar)s (default: %(default)s)",
59
+ metavar="DOMAIN",
60
+ )
61
+
62
+ mreg_args.add_argument(
63
+ "-p",
64
+ "--prompt",
65
+ help="default %(metavar)s), defaults to the server name if not set.",
66
+ metavar="PROMPT",
67
+ )
68
+
69
+ output_args = parser.add_argument_group("output settings")
70
+ output_args.add_argument(
71
+ "-v",
72
+ "--log-level",
73
+ dest="loglevel",
74
+ default="INFO",
75
+ choices=LogLevel.choices(),
76
+ help="Log level for logging.",
77
+ )
78
+ output_args.add_argument(
79
+ "-l",
80
+ "--logfile",
81
+ dest="logfile",
82
+ help="write log to %(metavar)s",
83
+ metavar="LOGFILE",
84
+ )
85
+ output_args.add_argument(
86
+ "--show-token",
87
+ dest="show_token",
88
+ action="store_true",
89
+ help="show API token after login",
90
+ )
91
+ output_args.add_argument(
92
+ "--record",
93
+ dest="record_traffic",
94
+ help="Record all server/client traffic to %(metavar)s",
95
+ metavar="RECFILE",
96
+ )
97
+ output_args.add_argument(
98
+ "--record-without-timestamps",
99
+ dest="record_traffic_without_timestamps",
100
+ action="store_true",
101
+ help="Do not apply timestamps to the recording file",
102
+ )
103
+ output_args.add_argument(
104
+ "--source",
105
+ dest="source",
106
+ help="Read commands from %(metavar)s",
107
+ metavar="SOURCE",
108
+ )
109
+
110
+ output_args.add_argument(
111
+ "--token-only",
112
+ dest="token_only",
113
+ action="store_true",
114
+ default=False,
115
+ help="Only attempt token login, this will avoid interactive prompts.",
116
+ )
117
+
118
+ output_args.add_argument(
119
+ "command", metavar="command", nargs="*", help="Oneshot command to issue to the cli."
120
+ )
121
+
122
+ args = parser.parse_args()
123
+
124
+ if args.version:
125
+ print(f"mreg-cli {__version__}")
126
+ raise SystemExit() from None
127
+
128
+ logfile = config.get_default_logfile()
129
+ conf = {k: v for k, v in vars(args).items() if v}
130
+ config.set_cmd_config(conf)
131
+
132
+ if "logfile" in conf:
133
+ logfile = conf["logfile"]
134
+
135
+ config.start_logging(logfile, args.loglevel)
136
+
137
+ logger.debug(f"args: {args}")
138
+
139
+ if "record_traffic" in conf:
140
+ OutputManager().recording_start(conf["record_traffic"])
141
+
142
+ if "record_traffic_without_timestamps" in conf:
143
+ OutputManager().record_timestamps(False)
144
+
145
+ if config.get("user") is None:
146
+ print("Username not set in config or as argument")
147
+ return
148
+ elif config.get("url") is None:
149
+ print("mreg url not set in config or as argument")
150
+ return
151
+
152
+ try:
153
+ try_token_or_login(
154
+ str(config.get("user")),
155
+ str(config.get("url")),
156
+ fail_without_token=args.token_only,
157
+ )
158
+
159
+ except (EOFError, KeyboardInterrupt, LoginFailedError) as e:
160
+ print(e)
161
+ raise SystemExit() from None
162
+ if args.show_token:
163
+ print(api.get_session_token() or "Token not found.")
164
+ raise SystemExit() from None
165
+
166
+ # Define a function that returns the prompt message
167
+ def get_prompt_message():
168
+ """Return the prompt message."""
169
+ manager = OutputManager()
170
+
171
+ if args.prompt:
172
+ prompt = args.prompt
173
+ elif config.get("prompt"):
174
+ prompt = config.get("prompt")
175
+ else:
176
+ host = str(config.get("url")).replace("https://", "").replace("http://", "")
177
+ user = args.user or config.get("user", "?")
178
+ prompt = f"{user}@{host}"
179
+
180
+ prefix: list[str] = []
181
+
182
+ if manager.recording_active():
183
+ prefix.append(f"&gt;'{manager.recording_filename()}'")
184
+
185
+ if prefix:
186
+ prefix_str = ",".join(prefix)
187
+ return HTML(f"[{prefix_str}] {prompt}> ")
188
+ return HTML(f"{prompt}> ")
189
+
190
+ # session is a PromptSession object from prompt_toolkit which handles
191
+ # some configurations of the prompt for us: the text of the prompt; the
192
+ # completer; and other visual things.
193
+ session = PromptSession(
194
+ message=get_prompt_message,
195
+ search_ignore_case=True,
196
+ completer=cli,
197
+ complete_while_typing=True,
198
+ complete_style=CompleteStyle.MULTI_COLUMN,
199
+ )
200
+
201
+ # Welcome text for the app
202
+ print("Type -h for help.")
203
+
204
+ # if the --source parameter was given, read commands from the source file and then exit
205
+ if "source" in conf:
206
+ logger.info("Reading commands from %s", conf["source"])
207
+ for command in source([conf["source"]], "verbosity" in conf, False):
208
+ cli.process_command_line(command)
209
+ return
210
+
211
+ # Check if we got a oneshot command. If so, execute it and exit.
212
+ if args.command:
213
+ cmd = " ".join(args.command)
214
+ try:
215
+ cli.process_command_line(cmd)
216
+ except ValueError as e:
217
+ print(e)
218
+
219
+ raise SystemExit() from None
220
+
221
+ # The app runs in an infinite loop and is expected to exit using sys.exit()
222
+ logger.debug("Entering main loop")
223
+ while True:
224
+ try:
225
+ lines = session.prompt()
226
+ except KeyboardInterrupt:
227
+ continue
228
+ except EOFError:
229
+ raise SystemExit() from None
230
+ try:
231
+ for line in lines.splitlines():
232
+ cli.process_command_line(line)
233
+ except ValueError as e:
234
+ print(e)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()