wexample-cli 0.3.0__tar.gz → 0.5.0__tar.gz
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.
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/PKG-INFO +4 -4
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/README.md +2 -2
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/pyproject.toml +2 -2
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/command/extended_command.py +119 -69
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/common/command_method_wrapper.py +13 -0
- wexample_cli-0.5.0/src/wexample_cli/decorator/screenable.py +115 -0
- wexample_cli-0.5.0/src/wexample_cli/helpers/extra_args.py +38 -0
- {wexample_cli-0.3.0/tests → wexample_cli-0.5.0/src/wexample_cli/testing}/__init__.py +0 -0
- wexample_cli-0.5.0/tests/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/command/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/common/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/const/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/const/middleware.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/const/types.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/context/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/context/execution_context.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/alias.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/as_sudo.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/command.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/middleware.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/option.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/option_stop_on_failure.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/webhook.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/exception/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/exception/abstract_command_option_exception.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/exception/command_option_missing_exception.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/exception/command_option_validation_exception.py +0 -0
- {wexample_cli-0.3.0/src/wexample_cli/middleware → wexample_cli-0.5.0/src/wexample_cli/helpers}/__init__.py +0 -0
- {wexample_cli-0.3.0/src/wexample_cli/testing → wexample_cli-0.5.0/src/wexample_cli/middleware}/__init__.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/middleware/abstract_middleware.py +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/py.typed +0 -0
- {wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/testing/kernel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: wexample-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Reusable CLI primitives — command decorators, options, middlewares, and the enriched command runner — extracted from wex-core so any kernel built on wexample-app can opt in without depending on the full wex framework.
|
|
5
5
|
Author-Email: weeger <contact@wexample.com>
|
|
6
6
|
License: MIT
|
|
@@ -10,7 +10,7 @@ Classifier: Operating System :: OS Independent
|
|
|
10
10
|
Project-URL: homepage, https://github.com/wexample/python-cli
|
|
11
11
|
Requires-Python: >=3.10
|
|
12
12
|
Requires-Dist: attrs>=23.1.0
|
|
13
|
-
Requires-Dist: wexample-app>=15.
|
|
13
|
+
Requires-Dist: wexample-app>=15.3.0
|
|
14
14
|
Requires-Dist: wexample-helpers>=13.1.0
|
|
15
15
|
Requires-Dist: wexample-prompt>=9.1.0
|
|
16
16
|
Provides-Extra: dev
|
|
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
|
|
21
21
|
# cli
|
|
22
22
|
|
|
23
|
-
Version: 0.
|
|
23
|
+
Version: 0.5.0
|
|
24
24
|
|
|
25
25
|
Reusable CLI primitives — command decorators, options, middlewares, and the enriched command runner — extracted from wex-core so any kernel built on wexample-app can opt in without depending on the full wex framework.
|
|
26
26
|
|
|
@@ -100,7 +100,7 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
|
|
|
100
100
|
## Dependencies
|
|
101
101
|
|
|
102
102
|
- attrs: >=23.1.0
|
|
103
|
-
- wexample-app: >=15.
|
|
103
|
+
- wexample-app: >=15.3.0
|
|
104
104
|
- wexample-helpers: >=13.1.0
|
|
105
105
|
- wexample-prompt: >=9.1.0
|
|
106
106
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cli
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
|
|
5
5
|
Reusable CLI primitives — command decorators, options, middlewares, and the enriched command runner — extracted from wex-core so any kernel built on wexample-app can opt in without depending on the full wex framework.
|
|
6
6
|
|
|
@@ -80,7 +80,7 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
|
|
|
80
80
|
## Dependencies
|
|
81
81
|
|
|
82
82
|
- attrs: >=23.1.0
|
|
83
|
-
- wexample-app: >=15.
|
|
83
|
+
- wexample-app: >=15.3.0
|
|
84
84
|
- wexample-helpers: >=13.1.0
|
|
85
85
|
- wexample-prompt: >=9.1.0
|
|
86
86
|
|
|
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "wexample-cli"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.5.0"
|
|
10
10
|
description = "Reusable CLI primitives — command decorators, options, middlewares, and the enriched command runner — extracted from wex-core so any kernel built on wexample-app can opt in without depending on the full wex framework."
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "weeger", email = "contact@wexample.com" },
|
|
@@ -19,7 +19,7 @@ classifiers = [
|
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
21
|
"attrs>=23.1.0",
|
|
22
|
-
"wexample-app>=15.
|
|
22
|
+
"wexample-app>=15.3.0",
|
|
23
23
|
"wexample-helpers>=13.1.0",
|
|
24
24
|
"wexample-prompt>=9.1.0",
|
|
25
25
|
]
|
|
@@ -25,16 +25,14 @@ class ExtendedCommand(Command):
|
|
|
25
25
|
# Now override function based on command_wrapper
|
|
26
26
|
self.function = self.command_wrapper.function
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
@staticmethod
|
|
29
|
+
def _wrap_dispatch(wrapper_fn: Any, inner: Any, request: CommandRequest) -> Any:
|
|
30
|
+
def wrapped(function_kwargs: dict[str, Any]) -> Any:
|
|
31
|
+
return wrapper_fn(inner, function_kwargs, request)
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
MIDDLEWARE_OPTION_VALUE_ALLWAYS,
|
|
35
|
-
MIDDLEWARE_OPTION_VALUE_OPTIONAL,
|
|
36
|
-
)
|
|
33
|
+
return wrapped
|
|
37
34
|
|
|
35
|
+
def execute_request(self, request: CommandRequest) -> Any:
|
|
38
36
|
# Instantiate middlewares first so their contributed options are visible
|
|
39
37
|
# to --help below and to option validation in _build_function_kwargs.
|
|
40
38
|
middlewares_attributes = self.command_wrapper.middlewares_attributes
|
|
@@ -55,6 +53,111 @@ class ExtendedCommand(Command):
|
|
|
55
53
|
|
|
56
54
|
function_kwargs = self._build_function_kwargs(request=request)
|
|
57
55
|
|
|
56
|
+
# Compose pipeline wrappers around the dispatch. Each wrapper has
|
|
57
|
+
# signature (next_dispatch, function_kwargs, request) -> response and
|
|
58
|
+
# decides whether to call next_dispatch (passthrough), call it multiple
|
|
59
|
+
# times (refresh loop), modify kwargs, etc. Generic mechanism — knows
|
|
60
|
+
# nothing about specific wrappers like @screenable.
|
|
61
|
+
dispatch = self._make_dispatch(request=request)
|
|
62
|
+
for wrapper_fn in reversed(self.command_wrapper.pipeline_wrappers):
|
|
63
|
+
dispatch = self._wrap_dispatch(wrapper_fn, dispatch, request)
|
|
64
|
+
|
|
65
|
+
return dispatch(function_kwargs)
|
|
66
|
+
|
|
67
|
+
def _build_function_kwargs(self, request: CommandRequest) -> dict[str, Any]:
|
|
68
|
+
from wexample_cli.exception.command_option_missing_exception import (
|
|
69
|
+
CommandOptionMissingException,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Allow middleware to add extra options.
|
|
73
|
+
for middleware in self.command_wrapper.middlewares:
|
|
74
|
+
middleware.append_options(
|
|
75
|
+
request=request,
|
|
76
|
+
command_wrapper=self.command_wrapper,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
"""Execute the command with the given request arguments."""
|
|
80
|
+
# Parse and convert arguments to appropriate types
|
|
81
|
+
parsed_args = self._parse_arguments(request.arguments)
|
|
82
|
+
|
|
83
|
+
"""Build the final kwargs dictionary for the function call."""
|
|
84
|
+
function_kwargs = {}
|
|
85
|
+
|
|
86
|
+
# Process all declared options
|
|
87
|
+
for option in self.command_wrapper.options:
|
|
88
|
+
value = None
|
|
89
|
+
|
|
90
|
+
# If the option is in parsed args, use that value
|
|
91
|
+
if option.name in parsed_args:
|
|
92
|
+
value = parsed_args[option.name]
|
|
93
|
+
# Otherwise, use the default value if available
|
|
94
|
+
elif option.default is not None:
|
|
95
|
+
value = option.default
|
|
96
|
+
# If the option is required but not provided, raise an error
|
|
97
|
+
elif option.required:
|
|
98
|
+
raise CommandOptionMissingException(option_name=option.name)
|
|
99
|
+
|
|
100
|
+
# Validate the value if validators are defined and value is not None
|
|
101
|
+
if value is not None and option.validators:
|
|
102
|
+
# For multiple values, validate each item individually
|
|
103
|
+
values_to_validate = value if isinstance(value, list) else [value]
|
|
104
|
+
|
|
105
|
+
for val in values_to_validate:
|
|
106
|
+
for validator in option.validators:
|
|
107
|
+
if not validator.validate(val):
|
|
108
|
+
from wexample_cli.exception.command_option_validation_exception import (
|
|
109
|
+
CommandOptionValidationException,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
raise CommandOptionValidationException(
|
|
113
|
+
option_name=option.name,
|
|
114
|
+
value=val,
|
|
115
|
+
error_message=validator.get_error_message(val),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Normalize to list if always_list is True
|
|
119
|
+
if value is not None and option.always_list and not isinstance(value, list):
|
|
120
|
+
value = [value]
|
|
121
|
+
|
|
122
|
+
# Assign the validated value
|
|
123
|
+
if value is not None:
|
|
124
|
+
option.value = function_kwargs[option.name] = value
|
|
125
|
+
|
|
126
|
+
# `--` passthrough: if the user supplied extra args after `--`, the
|
|
127
|
+
# parser put them in parsed_args["__extra_args__"]. Forward them as
|
|
128
|
+
# `extra_args` if the wrapped function declares that parameter;
|
|
129
|
+
# otherwise raise so misuse fails loudly instead of silently dropping
|
|
130
|
+
# input.
|
|
131
|
+
if "__extra_args__" in parsed_args:
|
|
132
|
+
import inspect
|
|
133
|
+
|
|
134
|
+
sig = inspect.signature(self.command_wrapper.function)
|
|
135
|
+
if "extra_args" in sig.parameters:
|
|
136
|
+
function_kwargs["extra_args"] = parsed_args["__extra_args__"]
|
|
137
|
+
else:
|
|
138
|
+
from wexample_app.exception.command_unexpected_argument_exception import (
|
|
139
|
+
CommandUnexpectedArgumentException,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
raise CommandUnexpectedArgumentException(
|
|
143
|
+
argument="--",
|
|
144
|
+
allowed_arguments=self.command_wrapper.get_options_names(),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return function_kwargs
|
|
148
|
+
|
|
149
|
+
def _execute_dispatch(
|
|
150
|
+
self, request: CommandRequest, function_kwargs: dict[str, Any]
|
|
151
|
+
) -> Any:
|
|
152
|
+
from wexample_app.helpers.response import response_normalize
|
|
153
|
+
from wexample_app.response.failure_response import FailureResponse
|
|
154
|
+
from wexample_app.response.multiple_response import MultipleResponse
|
|
155
|
+
|
|
156
|
+
from wexample_cli.const.middleware import (
|
|
157
|
+
MIDDLEWARE_OPTION_VALUE_ALLWAYS,
|
|
158
|
+
MIDDLEWARE_OPTION_VALUE_OPTIONAL,
|
|
159
|
+
)
|
|
160
|
+
|
|
58
161
|
if len(self.command_wrapper.middlewares) > 0:
|
|
59
162
|
output = MultipleResponse(kernel=self.kernel)
|
|
60
163
|
|
|
@@ -167,67 +270,6 @@ class ExtendedCommand(Command):
|
|
|
167
270
|
kernel=self.kernel, response=function_to_execute(**context.function_kwargs)
|
|
168
271
|
)
|
|
169
272
|
|
|
170
|
-
def _build_function_kwargs(self, request: CommandRequest) -> dict[str, Any]:
|
|
171
|
-
from wexample_cli.exception.command_option_missing_exception import (
|
|
172
|
-
CommandOptionMissingException,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
# Allow middleware to add extra options.
|
|
176
|
-
for middleware in self.command_wrapper.middlewares:
|
|
177
|
-
middleware.append_options(
|
|
178
|
-
request=request,
|
|
179
|
-
command_wrapper=self.command_wrapper,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
"""Execute the command with the given request arguments."""
|
|
183
|
-
# Parse and convert arguments to appropriate types
|
|
184
|
-
parsed_args = self._parse_arguments(request.arguments)
|
|
185
|
-
|
|
186
|
-
"""Build the final kwargs dictionary for the function call."""
|
|
187
|
-
function_kwargs = {}
|
|
188
|
-
|
|
189
|
-
# Process all declared options
|
|
190
|
-
for option in self.command_wrapper.options:
|
|
191
|
-
value = None
|
|
192
|
-
|
|
193
|
-
# If the option is in parsed args, use that value
|
|
194
|
-
if option.name in parsed_args:
|
|
195
|
-
value = parsed_args[option.name]
|
|
196
|
-
# Otherwise, use the default value if available
|
|
197
|
-
elif option.default is not None:
|
|
198
|
-
value = option.default
|
|
199
|
-
# If the option is required but not provided, raise an error
|
|
200
|
-
elif option.required:
|
|
201
|
-
raise CommandOptionMissingException(option_name=option.name)
|
|
202
|
-
|
|
203
|
-
# Validate the value if validators are defined and value is not None
|
|
204
|
-
if value is not None and option.validators:
|
|
205
|
-
# For multiple values, validate each item individually
|
|
206
|
-
values_to_validate = value if isinstance(value, list) else [value]
|
|
207
|
-
|
|
208
|
-
for val in values_to_validate:
|
|
209
|
-
for validator in option.validators:
|
|
210
|
-
if not validator.validate(val):
|
|
211
|
-
from wexample_cli.exception.command_option_validation_exception import (
|
|
212
|
-
CommandOptionValidationException,
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
raise CommandOptionValidationException(
|
|
216
|
-
option_name=option.name,
|
|
217
|
-
value=val,
|
|
218
|
-
error_message=validator.get_error_message(val),
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
# Normalize to list if always_list is True
|
|
222
|
-
if value is not None and option.always_list and not isinstance(value, list):
|
|
223
|
-
value = [value]
|
|
224
|
-
|
|
225
|
-
# Assign the validated value
|
|
226
|
-
if value is not None:
|
|
227
|
-
option.value = function_kwargs[option.name] = value
|
|
228
|
-
|
|
229
|
-
return function_kwargs
|
|
230
|
-
|
|
231
273
|
async def _execute_passes_parallel(
|
|
232
274
|
self, execution_contexts: list[ExecutionContext]
|
|
233
275
|
) -> list[Any]:
|
|
@@ -290,6 +332,14 @@ class ExtendedCommand(Command):
|
|
|
290
332
|
|
|
291
333
|
return responses
|
|
292
334
|
|
|
335
|
+
def _make_dispatch(self, request: CommandRequest) -> Any:
|
|
336
|
+
def dispatch(function_kwargs: dict[str, Any]) -> Any:
|
|
337
|
+
return self._execute_dispatch(
|
|
338
|
+
request=request, function_kwargs=function_kwargs
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return dispatch
|
|
342
|
+
|
|
293
343
|
def _parse_arguments(self, arguments: list[str] | dict) -> ParsedArgs:
|
|
294
344
|
"""Parse raw command line arguments into a dictionary of option name to value.
|
|
295
345
|
|
|
@@ -46,6 +46,15 @@ class CommandMethodWrapper(BaseClass):
|
|
|
46
46
|
factory=list,
|
|
47
47
|
description="List of command options available for this method",
|
|
48
48
|
)
|
|
49
|
+
pipeline_wrappers: list[AnyCallable] = public_field(
|
|
50
|
+
factory=list,
|
|
51
|
+
description=(
|
|
52
|
+
"Pipeline wrappers applied around the full dispatch. Each wrapper "
|
|
53
|
+
"has signature `(next_dispatch, function_kwargs, request) -> response` "
|
|
54
|
+
"and decides whether/how to invoke `next_dispatch(kwargs)`. "
|
|
55
|
+
"Registered in outer-to-inner order: the first registered wraps everything."
|
|
56
|
+
),
|
|
57
|
+
)
|
|
49
58
|
sudo: bool = public_field(
|
|
50
59
|
default=False,
|
|
51
60
|
description="If True, re-exec the entire process under sudo if not already root",
|
|
@@ -95,6 +104,10 @@ class CommandMethodWrapper(BaseClass):
|
|
|
95
104
|
|
|
96
105
|
self.middlewares_attributes[middleware_name] = middleware_kwargs
|
|
97
106
|
|
|
107
|
+
def register_pipeline_wrapper(self, wrapper_fn: AnyCallable) -> None:
|
|
108
|
+
"""Register a pipeline wrapper. See `pipeline_wrappers` field doc."""
|
|
109
|
+
self.pipeline_wrappers.append(wrapper_fn)
|
|
110
|
+
|
|
98
111
|
def set_middleware(self, middleware: AbstractMiddleware) -> None:
|
|
99
112
|
self.middlewares.append(middleware)
|
|
100
113
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from wexample_helpers.const.types import AnyCallable
|
|
7
|
+
|
|
8
|
+
from wexample_cli.common.command_method_wrapper import CommandMethodWrapper
|
|
9
|
+
|
|
10
|
+
OPTION_NAME_SCREEN: str = "screen"
|
|
11
|
+
OPTION_NAME_SCREEN_INTERVAL: str = "screen_interval"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def screenable(interval: float = 1.0, height: int = 30) -> AnyCallable:
|
|
15
|
+
"""Make a command refreshable via `--screen`.
|
|
16
|
+
|
|
17
|
+
Adds two CLI options to the wrapped command:
|
|
18
|
+
- `--screen` (flag): opt-in to continuous refresh.
|
|
19
|
+
- `--screen-interval <float>`: override the default interval.
|
|
20
|
+
|
|
21
|
+
Registers a pipeline wrapper that handles the refresh loop. The wrapper
|
|
22
|
+
pops the screen kwargs unconditionally (so they never leak to the user
|
|
23
|
+
function) and, when enabled, drives a `ScreenPromptResponse` whose callback
|
|
24
|
+
invokes the rest of the dispatch on every frame.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def decorator(command_wrapper: CommandMethodWrapper) -> CommandMethodWrapper:
|
|
28
|
+
from wexample_app.command.option import Option
|
|
29
|
+
|
|
30
|
+
from wexample_cli.common.command_method_wrapper import CommandMethodWrapper
|
|
31
|
+
|
|
32
|
+
if not isinstance(command_wrapper, CommandMethodWrapper):
|
|
33
|
+
raise TypeError(
|
|
34
|
+
"@screenable must decorate a CommandMethodWrapper produced by @command. "
|
|
35
|
+
"Apply @command first."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
command_wrapper.set_option(
|
|
39
|
+
Option(
|
|
40
|
+
name=OPTION_NAME_SCREEN,
|
|
41
|
+
type=bool,
|
|
42
|
+
description="Refresh the command continuously like `watch`.",
|
|
43
|
+
default=False,
|
|
44
|
+
is_flag=True,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
command_wrapper.set_option(
|
|
48
|
+
Option(
|
|
49
|
+
name=OPTION_NAME_SCREEN_INTERVAL,
|
|
50
|
+
type=float,
|
|
51
|
+
description="Override the refresh interval in seconds.",
|
|
52
|
+
default=None,
|
|
53
|
+
required=False,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
command_wrapper.register_pipeline_wrapper(
|
|
58
|
+
_build_screen_wrapper(default_interval=float(interval), height=int(height))
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return command_wrapper
|
|
62
|
+
|
|
63
|
+
return decorator
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_screen_wrapper(default_interval: float, height: int) -> AnyCallable:
|
|
67
|
+
"""Build the pipeline wrapper closure for one decorated command."""
|
|
68
|
+
|
|
69
|
+
def screen_wrapper(
|
|
70
|
+
next_dispatch: AnyCallable,
|
|
71
|
+
function_kwargs: dict[str, Any],
|
|
72
|
+
request: Any,
|
|
73
|
+
) -> Any:
|
|
74
|
+
# Always strip screen kwargs so they cannot leak to the user function
|
|
75
|
+
# or to inner pipeline wrappers, whether or not --screen was passed.
|
|
76
|
+
enabled = bool(function_kwargs.pop(OPTION_NAME_SCREEN, False))
|
|
77
|
+
interval_override = function_kwargs.pop(OPTION_NAME_SCREEN_INTERVAL, None)
|
|
78
|
+
|
|
79
|
+
if not enabled:
|
|
80
|
+
return next_dispatch(function_kwargs)
|
|
81
|
+
|
|
82
|
+
import time
|
|
83
|
+
|
|
84
|
+
from wexample_app.response.null_response import NullResponse
|
|
85
|
+
|
|
86
|
+
interval = (
|
|
87
|
+
float(interval_override)
|
|
88
|
+
if interval_override is not None
|
|
89
|
+
else default_interval
|
|
90
|
+
)
|
|
91
|
+
kernel_io = request.kernel.io
|
|
92
|
+
# Skip the inter-frame sleep on the very first frame so the user sees
|
|
93
|
+
# output immediately instead of waiting `interval` seconds before the
|
|
94
|
+
# first render.
|
|
95
|
+
is_first_frame = [True]
|
|
96
|
+
|
|
97
|
+
def _frame(response) -> None:
|
|
98
|
+
if not is_first_frame[0]:
|
|
99
|
+
time.sleep(interval)
|
|
100
|
+
is_first_frame[0] = False
|
|
101
|
+
|
|
102
|
+
response.clear()
|
|
103
|
+
saved_output = kernel_io.output
|
|
104
|
+
kernel_io.output = response.io.output
|
|
105
|
+
try:
|
|
106
|
+
next_dispatch(function_kwargs)
|
|
107
|
+
finally:
|
|
108
|
+
kernel_io.output = saved_output
|
|
109
|
+
|
|
110
|
+
response.reload()
|
|
111
|
+
|
|
112
|
+
kernel_io.screen(callback=_frame, height=height)
|
|
113
|
+
return NullResponse(kernel=request.kernel)
|
|
114
|
+
|
|
115
|
+
return screen_wrapper
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from wexample_cli.context.execution_context import ExecutionContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_shell_command(
|
|
11
|
+
*,
|
|
12
|
+
context: ExecutionContext,
|
|
13
|
+
command: str | None,
|
|
14
|
+
extra_args: list[str] | None,
|
|
15
|
+
) -> str | None:
|
|
16
|
+
"""Pick the shell command string from `--command "..."` or `-- args...`.
|
|
17
|
+
|
|
18
|
+
Pattern intended for commands that wrap an underlying shell call (SSH,
|
|
19
|
+
docker exec, sub-shell, ...). Returns the resolved command string ready
|
|
20
|
+
to be passed to the remote shell, or None if neither form was provided
|
|
21
|
+
(caller should then error/exit).
|
|
22
|
+
|
|
23
|
+
- `extra_args` (positional, post-`--`) wins over `command` (legacy `-c`),
|
|
24
|
+
because positional form is the one that round-trips complex quoting.
|
|
25
|
+
- Args in `extra_args` are joined with `shlex.join`, which single-quotes
|
|
26
|
+
tokens that need protection — `$VAR` is NOT expanded by the remote
|
|
27
|
+
shell, which is the safer default. Use the legacy `--command "..."`
|
|
28
|
+
form if you intentionally want remote expansion.
|
|
29
|
+
"""
|
|
30
|
+
if extra_args and command:
|
|
31
|
+
context.io.warning(
|
|
32
|
+
"Both --command and `-- <args>` were given; using `-- <args>`."
|
|
33
|
+
)
|
|
34
|
+
if extra_args:
|
|
35
|
+
return shlex.join(extra_args)
|
|
36
|
+
if command:
|
|
37
|
+
return command
|
|
38
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/decorator/option_stop_on_failure.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wexample_cli-0.3.0 → wexample_cli-0.5.0}/src/wexample_cli/middleware/abstract_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|