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.
Files changed (41) hide show
  1. osc_lib/api/api.py +67 -30
  2. osc_lib/api/auth.py +39 -25
  3. osc_lib/api/utils.py +10 -5
  4. osc_lib/cli/client_config.py +55 -35
  5. osc_lib/cli/format_columns.py +19 -17
  6. osc_lib/cli/identity.py +14 -3
  7. osc_lib/cli/pagination.py +83 -0
  8. osc_lib/cli/parseractions.py +116 -37
  9. osc_lib/clientmanager.py +49 -28
  10. osc_lib/command/command.py +20 -9
  11. osc_lib/command/timing.py +11 -1
  12. osc_lib/exceptions.py +13 -3
  13. osc_lib/logs.py +19 -9
  14. osc_lib/py.typed +0 -0
  15. osc_lib/shell.py +73 -56
  16. osc_lib/tests/api/fakes.py +1 -1
  17. osc_lib/tests/api/test_api.py +5 -5
  18. osc_lib/tests/api/test_utils.py +1 -1
  19. osc_lib/tests/cli/test_client_config.py +1 -1
  20. osc_lib/tests/cli/test_format_columns.py +1 -1
  21. osc_lib/tests/cli/test_parseractions.py +48 -100
  22. osc_lib/tests/command/test_timing.py +2 -2
  23. osc_lib/tests/fakes.py +10 -10
  24. osc_lib/tests/test_clientmanager.py +1 -1
  25. osc_lib/tests/test_logs.py +2 -2
  26. osc_lib/tests/test_shell.py +10 -10
  27. osc_lib/tests/utils/__init__.py +6 -25
  28. osc_lib/tests/utils/test_tags.py +22 -7
  29. osc_lib/tests/utils/test_utils.py +4 -14
  30. osc_lib/utils/__init__.py +183 -111
  31. osc_lib/utils/columns.py +25 -11
  32. osc_lib/utils/tags.py +39 -21
  33. {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/AUTHORS +1 -0
  34. {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/METADATA +11 -13
  35. osc_lib-4.0.0.dist-info/RECORD +53 -0
  36. {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/WHEEL +1 -1
  37. osc_lib-4.0.0.dist-info/pbr.json +1 -0
  38. osc_lib-3.1.0.dist-info/RECORD +0 -51
  39. osc_lib-3.1.0.dist-info/pbr.json +0 -1
  40. {osc_lib-3.1.0.dist-info → osc_lib-4.0.0.dist-info}/LICENSE +0 -0
  41. {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
+ )
@@ -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__(self, parser, namespace, values, option_string=None):
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.ArgumentTypeError(msg % str(values))
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.ArgumentTypeError(msg % str(values))
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__(self, parser, namespace, values, option_string=None):
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.ArgumentTypeError(msg % str(values))
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.ArgumentTypeError(msg % str(values))
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
- **kwargs
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(MultiKeyValueAction, self).__init__(
110
- option_strings, dest, **kwargs
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.ArgumentTypeError(
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.ArgumentTypeError(
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__(self, parser, namespace, values, metavar=None):
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 ArgumentTypeError
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.ArgumentTypeError(msg % str(kv))
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.ArgumentTypeError(msg % str(kv))
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__(self, parser, namespace, values, option_string=None):
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 ArgumentTypeError
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.ArgumentTypeError(msg % str(kv))
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] = "%s,%s" % (params[key], kv)
286
+ params[key] = f"{params[key]},{kv}"
228
287
  except KeyError:
229
288
  msg = _("A key=value pair is required: %s")
230
- raise argparse.ArgumentTypeError(msg % str(kv))
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__(self, parser, namespace, values, option_string=None):
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__(self, parser, namespace, values, option_string=None):
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.ArgumentTypeError(msg % str(option_string))
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(object):
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 ClientManager(object):
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=None,
64
- api_version=None,
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 self.auth_plugin_name.endswith(
149
- 'password'
150
- ) and not self._cli_options.auth.get('password'):
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.info('Using auth plugin: %s', self.auth_plugin_name)
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 = '%s_endpoint_override' % service_type.replace('-', '_')
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, service_type, region_name=None, interface='public'
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)
@@ -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__(mcs, name, bases, cls_dict):
28
- if 'log' not in cls_dict:
29
- cls_dict['log'] = logging.getLogger(
30
- cls_dict['__module__'] + '.' + name
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(CommandMeta, mcs).__new__(mcs, name, bases, cls_dict)
39
+ return super().__new__(mcs, name, bases, namespace)
33
40
 
34
41
 
35
42
  class Command(command.Command, metaclass=CommandMeta):
36
- def run(self, parsed_args):
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(Command, self).run(parsed_args)
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(self, old_option, new_option):
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
- def take_action(self, parsed_args):
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
- def __init__(self, code, message=None, details=None):
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 "%s (HTTP %s)" % (self.message, self.code)
80
+ def __str__(self) -> str:
81
+ return f"{self.message} (HTTP {self.code})"
72
82
 
73
83
 
74
84
  class BadRequest(ClientException):