osc-lib 3.1.0__py3-none-any.whl → 4.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.
- osc_lib/api/api.py +67 -30
- osc_lib/api/auth.py +39 -25
- osc_lib/api/utils.py +10 -5
- osc_lib/cli/client_config.py +55 -35
- osc_lib/cli/format_columns.py +19 -17
- osc_lib/cli/identity.py +14 -3
- osc_lib/cli/pagination.py +83 -0
- osc_lib/cli/parseractions.py +116 -37
- osc_lib/clientmanager.py +49 -28
- osc_lib/command/command.py +20 -9
- osc_lib/command/timing.py +11 -1
- osc_lib/exceptions.py +13 -3
- osc_lib/logs.py +19 -9
- osc_lib/py.typed +0 -0
- osc_lib/shell.py +73 -56
- osc_lib/tests/api/fakes.py +1 -1
- osc_lib/tests/api/test_api.py +5 -5
- osc_lib/tests/api/test_utils.py +1 -1
- osc_lib/tests/cli/test_client_config.py +1 -1
- osc_lib/tests/cli/test_format_columns.py +1 -1
- osc_lib/tests/cli/test_parseractions.py +48 -100
- osc_lib/tests/command/test_timing.py +2 -2
- osc_lib/tests/fakes.py +10 -10
- osc_lib/tests/test_clientmanager.py +1 -1
- osc_lib/tests/test_logs.py +2 -2
- osc_lib/tests/test_shell.py +10 -10
- osc_lib/tests/utils/__init__.py +6 -25
- osc_lib/tests/utils/test_tags.py +22 -7
- osc_lib/tests/utils/test_utils.py +4 -14
- osc_lib/utils/__init__.py +183 -111
- osc_lib/utils/columns.py +25 -11
- osc_lib/utils/tags.py +39 -21
- {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/AUTHORS +1 -0
- {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/METADATA +11 -13
- osc_lib-4.0.0.dist-info/RECORD +53 -0
- {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/WHEEL +1 -1
- osc_lib-4.0.0.dist-info/pbr.json +1 -0
- osc_lib-3.1.0.dist-info/RECORD +0 -51
- osc_lib-3.1.0.dist-info/pbr.json +0 -1
- {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/LICENSE +0 -0
- {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
2
|
+
# not use this file except in compliance with the License. You may obtain
|
3
|
+
# a copy of the License at
|
4
|
+
#
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
6
|
+
#
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
9
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
10
|
+
# License for the specific language governing permissions and limitations
|
11
|
+
# under the License.
|
12
|
+
|
13
|
+
import argparse
|
14
|
+
|
15
|
+
from osc_lib.cli import parseractions
|
16
|
+
from osc_lib.i18n import _
|
17
|
+
|
18
|
+
|
19
|
+
def add_marker_pagination_option_to_parser(
|
20
|
+
parser: argparse.ArgumentParser,
|
21
|
+
) -> None:
|
22
|
+
"""Add marker-based pagination options to the parser.
|
23
|
+
|
24
|
+
APIs that use marker-based paging use the marker and limit query parameters
|
25
|
+
to paginate through items in a collection.
|
26
|
+
|
27
|
+
Marker-based pagination is often used in cases where the length of the
|
28
|
+
total set of items is either changing frequently, or where the total length
|
29
|
+
might not be known upfront.
|
30
|
+
"""
|
31
|
+
parser.add_argument(
|
32
|
+
'--limit',
|
33
|
+
metavar='<limit>',
|
34
|
+
type=int,
|
35
|
+
action=parseractions.NonNegativeAction,
|
36
|
+
help=_(
|
37
|
+
'The maximum number of entries to return. If the value exceeds '
|
38
|
+
'the server-defined maximum, then the maximum value will be used.'
|
39
|
+
),
|
40
|
+
)
|
41
|
+
parser.add_argument(
|
42
|
+
'--marker',
|
43
|
+
metavar='<marker>',
|
44
|
+
default=None,
|
45
|
+
help=_(
|
46
|
+
'The first position in the collection to return results from. '
|
47
|
+
'This should be a value that was returned in a previous request.'
|
48
|
+
),
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
def add_offset_pagination_option_to_parser(
|
53
|
+
parser: argparse.ArgumentParser,
|
54
|
+
) -> None:
|
55
|
+
"""Add offset-based pagination options to the parser.
|
56
|
+
|
57
|
+
APIs that use offset-based paging use the offset and limit query parameters
|
58
|
+
to paginate through items in a collection.
|
59
|
+
|
60
|
+
Offset-based pagination is often used where the list of items is of a fixed
|
61
|
+
and predetermined length.
|
62
|
+
"""
|
63
|
+
parser.add_argument(
|
64
|
+
'--limit',
|
65
|
+
metavar='<limit>',
|
66
|
+
type=int,
|
67
|
+
action=parseractions.NonNegativeAction,
|
68
|
+
help=_(
|
69
|
+
'The maximum number of entries to return. If the value exceeds '
|
70
|
+
'the server-defined maximum, then the maximum value will be used.'
|
71
|
+
),
|
72
|
+
)
|
73
|
+
parser.add_argument(
|
74
|
+
'--offset',
|
75
|
+
metavar='<offset>',
|
76
|
+
type=int,
|
77
|
+
action=parseractions.NonNegativeAction,
|
78
|
+
default=None,
|
79
|
+
help=_(
|
80
|
+
'The (zero-based) offset of the first item in the collection to '
|
81
|
+
'return.'
|
82
|
+
),
|
83
|
+
)
|
osc_lib/cli/parseractions.py
CHANGED
@@ -16,9 +16,13 @@
|
|
16
16
|
"""argparse Custom Actions"""
|
17
17
|
|
18
18
|
import argparse
|
19
|
+
import collections.abc
|
20
|
+
import typing as ty
|
19
21
|
|
20
22
|
from osc_lib.i18n import _
|
21
23
|
|
24
|
+
_T = ty.TypeVar('_T')
|
25
|
+
|
22
26
|
|
23
27
|
class KeyValueAction(argparse.Action):
|
24
28
|
"""A custom action to parse arguments as key=value pairs
|
@@ -26,7 +30,16 @@ class KeyValueAction(argparse.Action):
|
|
26
30
|
Ensures that ``dest`` is a dict and values are strings.
|
27
31
|
"""
|
28
32
|
|
29
|
-
def __call__(
|
33
|
+
def __call__(
|
34
|
+
self,
|
35
|
+
parser: argparse.ArgumentParser,
|
36
|
+
namespace: argparse.Namespace,
|
37
|
+
values: ty.Union[str, ty.Sequence[ty.Any], None],
|
38
|
+
option_string: ty.Optional[str] = None,
|
39
|
+
) -> None:
|
40
|
+
if not isinstance(values, str):
|
41
|
+
raise TypeError('expected str')
|
42
|
+
|
30
43
|
# Make sure we have an empty dict rather than None
|
31
44
|
if getattr(namespace, self.dest, None) is None:
|
32
45
|
setattr(namespace, self.dest, {})
|
@@ -37,12 +50,12 @@ class KeyValueAction(argparse.Action):
|
|
37
50
|
# NOTE(qtang): Prevent null key setting in property
|
38
51
|
if '' == values_list[0]:
|
39
52
|
msg = _("Property key must be specified: %s")
|
40
|
-
raise argparse.
|
53
|
+
raise argparse.ArgumentError(self, msg % str(values))
|
41
54
|
else:
|
42
|
-
getattr(namespace, self.dest, {}).update([values_list])
|
55
|
+
getattr(namespace, self.dest, {}).update(dict([values_list]))
|
43
56
|
else:
|
44
57
|
msg = _("Expected 'key=value' type, but got: %s")
|
45
|
-
raise argparse.
|
58
|
+
raise argparse.ArgumentError(self, msg % str(values))
|
46
59
|
|
47
60
|
|
48
61
|
class KeyValueAppendAction(argparse.Action):
|
@@ -51,7 +64,16 @@ class KeyValueAppendAction(argparse.Action):
|
|
51
64
|
Ensures that ``dest`` is a dict and values are lists of strings.
|
52
65
|
"""
|
53
66
|
|
54
|
-
def __call__(
|
67
|
+
def __call__(
|
68
|
+
self,
|
69
|
+
parser: argparse.ArgumentParser,
|
70
|
+
namespace: argparse.Namespace,
|
71
|
+
values: ty.Union[str, ty.Sequence[ty.Any], None],
|
72
|
+
option_string: ty.Optional[str] = None,
|
73
|
+
) -> None:
|
74
|
+
if not isinstance(values, str):
|
75
|
+
raise TypeError('expected str')
|
76
|
+
|
55
77
|
# Make sure we have an empty dict rather than None
|
56
78
|
if getattr(namespace, self.dest, None) is None:
|
57
79
|
setattr(namespace, self.dest, {})
|
@@ -62,7 +84,7 @@ class KeyValueAppendAction(argparse.Action):
|
|
62
84
|
# NOTE(qtang): Prevent null key setting in property
|
63
85
|
if '' == key:
|
64
86
|
msg = _("Property key must be specified: %s")
|
65
|
-
raise argparse.
|
87
|
+
raise argparse.ArgumentError(self, msg % str(values))
|
66
88
|
|
67
89
|
dest = getattr(namespace, self.dest)
|
68
90
|
if key in dest:
|
@@ -71,7 +93,7 @@ class KeyValueAppendAction(argparse.Action):
|
|
71
93
|
dest[key] = [value]
|
72
94
|
else:
|
73
95
|
msg = _("Expected 'key=value' type, but got: %s")
|
74
|
-
raise argparse.
|
96
|
+
raise argparse.ArgumentError(self, msg % str(values))
|
75
97
|
|
76
98
|
|
77
99
|
class MultiKeyValueAction(argparse.Action):
|
@@ -86,13 +108,19 @@ class MultiKeyValueAction(argparse.Action):
|
|
86
108
|
|
87
109
|
def __init__(
|
88
110
|
self,
|
89
|
-
option_strings,
|
90
|
-
dest,
|
91
|
-
nargs=None,
|
92
|
-
required_keys=None,
|
93
|
-
optional_keys=None,
|
94
|
-
|
95
|
-
|
111
|
+
option_strings: ty.Sequence[str],
|
112
|
+
dest: str,
|
113
|
+
nargs: ty.Union[int, str, None] = None,
|
114
|
+
required_keys: ty.Optional[ty.Sequence[str]] = None,
|
115
|
+
optional_keys: ty.Optional[ty.Sequence[str]] = None,
|
116
|
+
const: ty.Optional[_T] = None,
|
117
|
+
default: ty.Union[_T, str, None] = None,
|
118
|
+
type: ty.Optional[collections.abc.Callable[[str], _T]] = None,
|
119
|
+
choices: ty.Optional[collections.abc.Iterable[_T]] = None,
|
120
|
+
required: bool = False,
|
121
|
+
help: ty.Optional[str] = None,
|
122
|
+
metavar: ty.Union[str, tuple[str, ...], None] = None,
|
123
|
+
) -> None:
|
96
124
|
"""Initialize the action object, and parse customized options
|
97
125
|
|
98
126
|
Required keys and optional keys can be specified when initializing
|
@@ -106,14 +134,24 @@ class MultiKeyValueAction(argparse.Action):
|
|
106
134
|
msg = _("Parameter 'nargs' is not allowed, but got %s")
|
107
135
|
raise ValueError(msg % nargs)
|
108
136
|
|
109
|
-
super(
|
110
|
-
option_strings,
|
137
|
+
super().__init__(
|
138
|
+
option_strings,
|
139
|
+
dest,
|
140
|
+
nargs=nargs,
|
141
|
+
const=const,
|
142
|
+
default=default,
|
143
|
+
type=type,
|
144
|
+
choices=choices,
|
145
|
+
required=required,
|
146
|
+
help=help,
|
147
|
+
metavar=metavar,
|
111
148
|
)
|
112
149
|
|
113
150
|
# required_keys: A list of keys that is required. None by default.
|
114
151
|
if required_keys and not isinstance(required_keys, list):
|
115
152
|
msg = _("'required_keys' must be a list")
|
116
153
|
raise TypeError(msg)
|
154
|
+
|
117
155
|
self.required_keys = set(required_keys or [])
|
118
156
|
|
119
157
|
# optional_keys: A list of keys that is optional. None by default.
|
@@ -122,7 +160,7 @@ class MultiKeyValueAction(argparse.Action):
|
|
122
160
|
raise TypeError(msg)
|
123
161
|
self.optional_keys = set(optional_keys or [])
|
124
162
|
|
125
|
-
def validate_keys(self, keys):
|
163
|
+
def validate_keys(self, keys: ty.Sequence[str]) -> None:
|
126
164
|
"""Validate the provided keys.
|
127
165
|
|
128
166
|
:param keys: A list of keys to validate.
|
@@ -136,12 +174,13 @@ class MultiKeyValueAction(argparse.Action):
|
|
136
174
|
"Invalid keys %(invalid_keys)s specified.\n"
|
137
175
|
"Valid keys are: %(valid_keys)s"
|
138
176
|
)
|
139
|
-
raise argparse.
|
177
|
+
raise argparse.ArgumentError(
|
178
|
+
self,
|
140
179
|
msg
|
141
180
|
% {
|
142
181
|
'invalid_keys': ', '.join(invalid_keys),
|
143
182
|
'valid_keys': ', '.join(valid_keys),
|
144
|
-
}
|
183
|
+
},
|
145
184
|
)
|
146
185
|
|
147
186
|
if self.required_keys:
|
@@ -151,35 +190,45 @@ class MultiKeyValueAction(argparse.Action):
|
|
151
190
|
"Missing required keys %(missing_keys)s.\n"
|
152
191
|
"Required keys are: %(required_keys)s"
|
153
192
|
)
|
154
|
-
raise argparse.
|
193
|
+
raise argparse.ArgumentError(
|
194
|
+
self,
|
155
195
|
msg
|
156
196
|
% {
|
157
197
|
'missing_keys': ', '.join(missing_keys),
|
158
198
|
'required_keys': ', '.join(self.required_keys),
|
159
|
-
}
|
199
|
+
},
|
160
200
|
)
|
161
201
|
|
162
|
-
def __call__(
|
202
|
+
def __call__(
|
203
|
+
self,
|
204
|
+
parser: argparse.ArgumentParser,
|
205
|
+
namespace: argparse.Namespace,
|
206
|
+
values: ty.Union[str, ty.Sequence[ty.Any], None],
|
207
|
+
option_string: ty.Optional[str] = None,
|
208
|
+
) -> None:
|
209
|
+
if not isinstance(values, str):
|
210
|
+
raise TypeError('expected str')
|
211
|
+
|
163
212
|
# Make sure we have an empty list rather than None
|
164
213
|
if getattr(namespace, self.dest, None) is None:
|
165
214
|
setattr(namespace, self.dest, [])
|
166
215
|
|
167
|
-
params = {}
|
216
|
+
params: dict[str, str] = {}
|
168
217
|
for kv in values.split(','):
|
169
|
-
# Add value if an assignment else raise
|
218
|
+
# Add value if an assignment else raise ArgumentError
|
170
219
|
if '=' in kv:
|
171
220
|
kv_list = kv.split('=', 1)
|
172
221
|
# NOTE(qtang): Prevent null key setting in property
|
173
222
|
if '' == kv_list[0]:
|
174
223
|
msg = _("Each property key must be specified: %s")
|
175
|
-
raise argparse.
|
224
|
+
raise argparse.ArgumentError(self, msg % str(kv))
|
176
225
|
else:
|
177
|
-
params.update([kv_list])
|
226
|
+
params.update(dict([kv_list]))
|
178
227
|
else:
|
179
228
|
msg = _(
|
180
229
|
"Expected comma separated 'key=value' pairs, but got: %s"
|
181
230
|
)
|
182
|
-
raise argparse.
|
231
|
+
raise argparse.ArgumentError(self, msg % str(kv))
|
183
232
|
|
184
233
|
# Check key validation
|
185
234
|
self.validate_keys(list(params))
|
@@ -196,38 +245,48 @@ class MultiKeyValueCommaAction(MultiKeyValueAction):
|
|
196
245
|
Ex. key1=val1,val2,key2=val3 => {"key1": "val1,val2", "key2": "val3"}
|
197
246
|
"""
|
198
247
|
|
199
|
-
def __call__(
|
248
|
+
def __call__(
|
249
|
+
self,
|
250
|
+
parser: argparse.ArgumentParser,
|
251
|
+
namespace: argparse.Namespace,
|
252
|
+
values: ty.Union[str, ty.Sequence[ty.Any], None],
|
253
|
+
option_string: ty.Optional[str] = None,
|
254
|
+
) -> None:
|
200
255
|
"""Overwrite the __call__ function of MultiKeyValueAction
|
201
256
|
|
202
257
|
This is done to handle scenarios where we may have comma seperated
|
203
258
|
data as a single value.
|
204
259
|
"""
|
260
|
+
if not isinstance(values, str):
|
261
|
+
msg = _("Invalid key=value pair, non-string value provided: %s")
|
262
|
+
raise argparse.ArgumentError(self, msg % str(values))
|
263
|
+
|
205
264
|
# Make sure we have an empty list rather than None
|
206
265
|
if getattr(namespace, self.dest, None) is None:
|
207
266
|
setattr(namespace, self.dest, [])
|
208
267
|
|
209
|
-
params = {}
|
268
|
+
params: dict[str, str] = {}
|
210
269
|
key = ''
|
211
270
|
for kv in values.split(','):
|
212
|
-
# Add value if an assignment else raise
|
271
|
+
# Add value if an assignment else raise ArgumentError
|
213
272
|
if '=' in kv:
|
214
273
|
kv_list = kv.split('=', 1)
|
215
274
|
# NOTE(qtang): Prevent null key setting in property
|
216
275
|
if '' == kv_list[0]:
|
217
276
|
msg = _("A key must be specified before '=': %s")
|
218
|
-
raise argparse.
|
277
|
+
raise argparse.ArgumentError(self, msg % str(kv))
|
219
278
|
else:
|
220
|
-
params.update([kv_list])
|
279
|
+
params.update(dict([kv_list]))
|
221
280
|
key = kv_list[0]
|
222
281
|
else:
|
223
282
|
# If the ',' split does not have key=value pair, then it
|
224
283
|
# means the current value is a part of the previous
|
225
284
|
# key=value pair, so append it.
|
226
285
|
try:
|
227
|
-
params[key] = "
|
286
|
+
params[key] = f"{params[key]},{kv}"
|
228
287
|
except KeyError:
|
229
288
|
msg = _("A key=value pair is required: %s")
|
230
|
-
raise argparse.
|
289
|
+
raise argparse.ArgumentError(self, msg % str(kv))
|
231
290
|
|
232
291
|
# Check key validation
|
233
292
|
self.validate_keys(list(params))
|
@@ -245,7 +304,17 @@ class RangeAction(argparse.Action):
|
|
245
304
|
'6:9' sets ``dest`` to (6, 9)
|
246
305
|
"""
|
247
306
|
|
248
|
-
def __call__(
|
307
|
+
def __call__(
|
308
|
+
self,
|
309
|
+
parser: argparse.ArgumentParser,
|
310
|
+
namespace: argparse.Namespace,
|
311
|
+
values: ty.Union[str, ty.Sequence[ty.Any], None],
|
312
|
+
option_string: ty.Optional[str] = None,
|
313
|
+
) -> None:
|
314
|
+
if not isinstance(values, str):
|
315
|
+
msg = _("Invalid range, non-string value provided")
|
316
|
+
raise argparse.ArgumentError(self, msg)
|
317
|
+
|
249
318
|
range = values.split(':')
|
250
319
|
if len(range) == 0:
|
251
320
|
# Nothing passed, return a zero default
|
@@ -279,9 +348,19 @@ class NonNegativeAction(argparse.Action):
|
|
279
348
|
Ensures the value is >= 0.
|
280
349
|
"""
|
281
350
|
|
282
|
-
def __call__(
|
351
|
+
def __call__(
|
352
|
+
self,
|
353
|
+
parser: argparse.ArgumentParser,
|
354
|
+
namespace: argparse.Namespace,
|
355
|
+
values: ty.Union[str, ty.Sequence[ty.Any], None],
|
356
|
+
option_string: ty.Optional[str] = None,
|
357
|
+
) -> None:
|
358
|
+
if not isinstance(values, (str, int, float)):
|
359
|
+
msg = _("%s expected a non-negative integer")
|
360
|
+
raise argparse.ArgumentError(self, msg % str(option_string))
|
361
|
+
|
283
362
|
if int(values) >= 0:
|
284
363
|
setattr(namespace, self.dest, values)
|
285
364
|
else:
|
286
365
|
msg = _("%s expected a non-negative integer")
|
287
|
-
raise argparse.
|
366
|
+
raise argparse.ArgumentError(self, msg % str(option_string))
|
osc_lib/clientmanager.py
CHANGED
@@ -17,7 +17,12 @@
|
|
17
17
|
|
18
18
|
import copy
|
19
19
|
import logging
|
20
|
+
import typing as ty
|
21
|
+
import warnings
|
20
22
|
|
23
|
+
from keystoneauth1 import access as ksa_access
|
24
|
+
from keystoneauth1 import session as ksa_session
|
25
|
+
from openstack.config import cloud_region
|
21
26
|
from openstack.config import loader as config # noqa
|
22
27
|
from openstack import connection
|
23
28
|
from oslo_utils import strutils
|
@@ -25,20 +30,23 @@ from oslo_utils import strutils
|
|
25
30
|
from osc_lib.api import auth
|
26
31
|
from osc_lib import exceptions
|
27
32
|
|
28
|
-
|
29
33
|
LOG = logging.getLogger(__name__)
|
30
34
|
|
31
|
-
PLUGIN_MODULES = []
|
32
|
-
|
33
35
|
|
34
|
-
class ClientCache
|
36
|
+
class ClientCache:
|
35
37
|
"""Descriptor class for caching created client handles."""
|
36
38
|
|
37
|
-
def __init__(self, factory):
|
39
|
+
def __init__(self, factory: ty.Any) -> None:
|
40
|
+
warnings.warn(
|
41
|
+
"The ClientCache class is deprecated for removal as it has no "
|
42
|
+
"users.",
|
43
|
+
category=DeprecationWarning,
|
44
|
+
)
|
45
|
+
|
38
46
|
self.factory = factory
|
39
47
|
self._handle = None
|
40
48
|
|
41
|
-
def __get__(self, instance, owner):
|
49
|
+
def __get__(self, instance: ty.Any, owner: ty.Any) -> ty.Any:
|
42
50
|
# Tell the ClientManager to login to keystone
|
43
51
|
if self._handle is None:
|
44
52
|
try:
|
@@ -50,9 +58,15 @@ class ClientCache(object):
|
|
50
58
|
return self._handle
|
51
59
|
|
52
60
|
|
53
|
-
class
|
61
|
+
class _PasswordHelper(ty.Protocol):
|
62
|
+
def __call__(self, prompt: ty.Optional[str] = None) -> str: ...
|
63
|
+
|
64
|
+
|
65
|
+
class ClientManager:
|
54
66
|
"""Manages access to API clients, including authentication."""
|
55
67
|
|
68
|
+
session: ksa_session.Session
|
69
|
+
|
56
70
|
# NOTE(dtroyer): Keep around the auth required state of the _current_
|
57
71
|
# command since ClientManager has no visibility to the
|
58
72
|
# command itself; assume auth is not required.
|
@@ -60,12 +74,12 @@ class ClientManager(object):
|
|
60
74
|
|
61
75
|
def __init__(
|
62
76
|
self,
|
63
|
-
cli_options
|
64
|
-
api_version
|
65
|
-
pw_func=None,
|
66
|
-
app_name=None,
|
67
|
-
app_version=None,
|
68
|
-
):
|
77
|
+
cli_options: cloud_region.CloudRegion,
|
78
|
+
api_version: ty.Optional[dict[str, str]],
|
79
|
+
pw_func: ty.Optional[_PasswordHelper] = None,
|
80
|
+
app_name: ty.Optional[str] = None,
|
81
|
+
app_version: ty.Optional[str] = None,
|
82
|
+
) -> None:
|
69
83
|
"""Set up a ClientManager
|
70
84
|
|
71
85
|
:param cli_options:
|
@@ -93,7 +107,6 @@ class ClientManager(object):
|
|
93
107
|
self.timing = self._cli_options.timing
|
94
108
|
|
95
109
|
self._auth_ref = None
|
96
|
-
self.session = None
|
97
110
|
|
98
111
|
# self.verify is the Requests-compatible form
|
99
112
|
# self.cacert is the form used by the legacy client libs
|
@@ -124,7 +137,7 @@ class ClientManager(object):
|
|
124
137
|
# prior to dereferrencing auth_ref.
|
125
138
|
self._auth_setup_completed = False
|
126
139
|
|
127
|
-
def setup_auth(self):
|
140
|
+
def setup_auth(self) -> None:
|
128
141
|
"""Set up authentication
|
129
142
|
|
130
143
|
This is deferred until authentication is actually attempted because
|
@@ -145,12 +158,14 @@ class ClientManager(object):
|
|
145
158
|
|
146
159
|
# Horrible hack alert...must handle prompt for null password if
|
147
160
|
# password auth is requested.
|
148
|
-
if
|
149
|
-
'password'
|
150
|
-
|
161
|
+
if (
|
162
|
+
self.auth_plugin_name.endswith('password')
|
163
|
+
and not self._cli_options.auth.get('password')
|
164
|
+
and self._pw_callback is not None
|
165
|
+
):
|
151
166
|
self._cli_options.auth['password'] = self._pw_callback()
|
152
167
|
|
153
|
-
LOG.
|
168
|
+
LOG.debug('Using auth plugin: %s', self.auth_plugin_name)
|
154
169
|
LOG.debug(
|
155
170
|
'Using parameters %s',
|
156
171
|
strutils.mask_password(self._cli_options.auth),
|
@@ -177,7 +192,10 @@ class ClientManager(object):
|
|
177
192
|
|
178
193
|
self._auth_setup_completed = True
|
179
194
|
|
180
|
-
def validate_scope(self):
|
195
|
+
def validate_scope(self) -> None:
|
196
|
+
if not self._auth_ref:
|
197
|
+
raise Exception('no authentication information')
|
198
|
+
|
181
199
|
if self._auth_ref.project_id is not None:
|
182
200
|
# We already have a project scope.
|
183
201
|
return
|
@@ -194,7 +212,7 @@ class ClientManager(object):
|
|
194
212
|
)
|
195
213
|
|
196
214
|
@property
|
197
|
-
def auth_ref(self):
|
215
|
+
def auth_ref(self) -> ty.Optional[ksa_access.AccessInfo]:
|
198
216
|
"""Dereference will trigger an auth if it hasn't already"""
|
199
217
|
if (
|
200
218
|
not self._auth_required
|
@@ -208,11 +226,11 @@ class ClientManager(object):
|
|
208
226
|
self._auth_ref = self.auth.get_auth_ref(self.session)
|
209
227
|
return self._auth_ref
|
210
228
|
|
211
|
-
def _override_for(self, service_type):
|
212
|
-
key = '
|
213
|
-
return self._cli_options.config.get(key)
|
229
|
+
def _override_for(self, service_type: str) -> ty.Optional[str]:
|
230
|
+
key = '{}_endpoint_override'.format(service_type.replace('-', '_'))
|
231
|
+
return ty.cast(ty.Optional[str], self._cli_options.config.get(key))
|
214
232
|
|
215
|
-
def is_service_available(self, service_type):
|
233
|
+
def is_service_available(self, service_type: str) -> ty.Optional[bool]:
|
216
234
|
"""Check if a service type is in the current Service Catalog"""
|
217
235
|
# If there is an override, assume the service is available
|
218
236
|
if self._override_for(service_type):
|
@@ -236,8 +254,11 @@ class ClientManager(object):
|
|
236
254
|
return service_available
|
237
255
|
|
238
256
|
def get_endpoint_for_service_type(
|
239
|
-
self,
|
240
|
-
|
257
|
+
self,
|
258
|
+
service_type: str,
|
259
|
+
region_name: ty.Optional[str] = None,
|
260
|
+
interface: str = 'public',
|
261
|
+
) -> ty.Optional[str]:
|
241
262
|
"""Return the endpoint URL for the service type."""
|
242
263
|
# Overrides take priority unconditionally
|
243
264
|
override = self._override_for(service_type)
|
@@ -262,5 +283,5 @@ class ClientManager(object):
|
|
262
283
|
)
|
263
284
|
return endpoint
|
264
285
|
|
265
|
-
def get_configuration(self):
|
286
|
+
def get_configuration(self) -> dict[str, ty.Any]:
|
266
287
|
return copy.deepcopy(self._cli_options.config)
|
osc_lib/command/command.py
CHANGED
@@ -13,7 +13,9 @@
|
|
13
13
|
# under the License.
|
14
14
|
|
15
15
|
import abc
|
16
|
+
import argparse
|
16
17
|
import logging
|
18
|
+
import typing as ty
|
17
19
|
|
18
20
|
from cliff import command
|
19
21
|
from cliff import lister
|
@@ -24,20 +26,27 @@ from osc_lib.i18n import _
|
|
24
26
|
|
25
27
|
|
26
28
|
class CommandMeta(abc.ABCMeta):
|
27
|
-
def __new__(
|
28
|
-
|
29
|
-
|
30
|
-
|
29
|
+
def __new__(
|
30
|
+
mcs: type['CommandMeta'],
|
31
|
+
name: str,
|
32
|
+
bases: tuple[type[ty.Any], ...],
|
33
|
+
namespace: dict[str, ty.Any],
|
34
|
+
) -> 'CommandMeta':
|
35
|
+
if 'log' not in namespace:
|
36
|
+
namespace['log'] = logging.getLogger(
|
37
|
+
namespace['__module__'] + '.' + name
|
31
38
|
)
|
32
|
-
return super(
|
39
|
+
return super().__new__(mcs, name, bases, namespace)
|
33
40
|
|
34
41
|
|
35
42
|
class Command(command.Command, metaclass=CommandMeta):
|
36
|
-
|
43
|
+
log: logging.Logger
|
44
|
+
|
45
|
+
def run(self, parsed_args: argparse.Namespace) -> int:
|
37
46
|
self.log.debug('run(%s)', parsed_args)
|
38
|
-
return super(
|
47
|
+
return super().run(parsed_args)
|
39
48
|
|
40
|
-
def validate_os_beta_command_enabled(self):
|
49
|
+
def validate_os_beta_command_enabled(self) -> None:
|
41
50
|
if not self.app.options.os_beta_command:
|
42
51
|
msg = _(
|
43
52
|
'Caution: This is a beta command and subject to '
|
@@ -46,7 +55,9 @@ class Command(command.Command, metaclass=CommandMeta):
|
|
46
55
|
)
|
47
56
|
raise exceptions.CommandError(msg)
|
48
57
|
|
49
|
-
def deprecated_option_warning(
|
58
|
+
def deprecated_option_warning(
|
59
|
+
self, old_option: str, new_option: str
|
60
|
+
) -> None:
|
50
61
|
"""Emit a warning for use of a deprecated option"""
|
51
62
|
self.log.warning(
|
52
63
|
_("The %(old)s option is deprecated, please use %(new)s instead.")
|
osc_lib/command/timing.py
CHANGED
@@ -13,13 +13,23 @@
|
|
13
13
|
|
14
14
|
"""Timing Implementation"""
|
15
15
|
|
16
|
+
import argparse
|
17
|
+
import typing as ty
|
18
|
+
|
16
19
|
from osc_lib.command import command
|
17
20
|
|
21
|
+
if ty.TYPE_CHECKING:
|
22
|
+
from osc_lib import shell
|
23
|
+
|
18
24
|
|
19
25
|
class Timing(command.Lister):
|
20
26
|
"""Show timing data"""
|
21
27
|
|
22
|
-
|
28
|
+
app: 'shell.OpenStackShell'
|
29
|
+
|
30
|
+
def take_action(
|
31
|
+
self, parsed_args: argparse.Namespace
|
32
|
+
) -> tuple[tuple[str, ...], list[ty.Any]]:
|
23
33
|
column_headers = (
|
24
34
|
'URL',
|
25
35
|
'Seconds',
|
osc_lib/exceptions.py
CHANGED
@@ -15,6 +15,8 @@
|
|
15
15
|
|
16
16
|
"""Exception definitions."""
|
17
17
|
|
18
|
+
import typing as ty
|
19
|
+
|
18
20
|
|
19
21
|
class CommandError(Exception):
|
20
22
|
pass
|
@@ -59,7 +61,15 @@ class InvalidValue(Exception):
|
|
59
61
|
class ClientException(Exception):
|
60
62
|
"""The base exception class for all exceptions this library raises."""
|
61
63
|
|
62
|
-
|
64
|
+
http_status: int
|
65
|
+
message: str
|
66
|
+
|
67
|
+
def __init__(
|
68
|
+
self,
|
69
|
+
code: ty.Union[int, str],
|
70
|
+
message: ty.Optional[str] = None,
|
71
|
+
details: ty.Optional[str] = None,
|
72
|
+
):
|
63
73
|
if not isinstance(code, int) and message is None:
|
64
74
|
message = code
|
65
75
|
code = self.http_status
|
@@ -67,8 +77,8 @@ class ClientException(Exception):
|
|
67
77
|
self.message = message or self.__class__.message
|
68
78
|
self.details = details
|
69
79
|
|
70
|
-
def __str__(self):
|
71
|
-
return "
|
80
|
+
def __str__(self) -> str:
|
81
|
+
return f"{self.message} (HTTP {self.code})"
|
72
82
|
|
73
83
|
|
74
84
|
class BadRequest(ClientException):
|