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
osc_lib/logs.py CHANGED
@@ -13,19 +13,24 @@
13
13
 
14
14
  """Application logging"""
15
15
 
16
+ import argparse
17
+ import collections.abc
16
18
  import logging
17
19
  import sys
20
+ import typing as ty
18
21
  import warnings
19
22
 
23
+ from openstack.config import cloud_config
20
24
 
21
- def get_loggers():
25
+
26
+ def get_loggers() -> dict[str, str]:
22
27
  loggers = {}
23
28
  for logkey in logging.Logger.manager.loggerDict.keys():
24
29
  loggers[logkey] = logging.getLevelName(logging.getLogger(logkey).level)
25
30
  return loggers
26
31
 
27
32
 
28
- def log_level_from_options(options):
33
+ def log_level_from_options(options: argparse.Namespace) -> int:
29
34
  # if --debug, --quiet or --verbose is not specified,
30
35
  # the default logging level is warning
31
36
  log_level = logging.WARNING
@@ -41,7 +46,7 @@ def log_level_from_options(options):
41
46
  return log_level
42
47
 
43
48
 
44
- def log_level_from_string(level_string):
49
+ def log_level_from_string(level_string: str) -> int:
45
50
  log_level = {
46
51
  'critical': logging.CRITICAL,
47
52
  'error': logging.ERROR,
@@ -52,7 +57,7 @@ def log_level_from_string(level_string):
52
57
  return log_level
53
58
 
54
59
 
55
- def log_level_from_config(config):
60
+ def log_level_from_config(config: collections.abc.Mapping[str, ty.Any]) -> int:
56
61
  # Check the command line option
57
62
  verbose_level = config.get('verbose_level')
58
63
  if config.get('debug', False):
@@ -70,7 +75,7 @@ def log_level_from_config(config):
70
75
  return log_level_from_string(verbose_level)
71
76
 
72
77
 
73
- def set_warning_filter(log_level):
78
+ def set_warning_filter(log_level: int) -> None:
74
79
  if log_level == logging.ERROR:
75
80
  warnings.simplefilter("ignore")
76
81
  elif log_level == logging.WARNING:
@@ -89,7 +94,12 @@ class _FileFormatter(logging.Formatter):
89
94
  _LOG_MESSAGE_END = '%(message)s'
90
95
  _LOG_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
91
96
 
92
- def __init__(self, options=None, config=None, **kwargs):
97
+ def __init__(
98
+ self,
99
+ options: ty.Optional[argparse.Namespace] = None,
100
+ config: ty.Optional[cloud_config.CloudConfig] = None,
101
+ **kwargs: ty.Any,
102
+ ) -> None:
93
103
  context = {}
94
104
  if options:
95
105
  context = {
@@ -114,10 +124,10 @@ class _FileFormatter(logging.Formatter):
114
124
  logging.Formatter.__init__(self, self.fmt, self._LOG_DATE_FORMAT)
115
125
 
116
126
 
117
- class LogConfigurator(object):
127
+ class LogConfigurator:
118
128
  _CONSOLE_MESSAGE_FORMAT = '%(message)s'
119
129
 
120
- def __init__(self, options):
130
+ def __init__(self, options: argparse.Namespace) -> None:
121
131
  self.root_logger = logging.getLogger('')
122
132
  self.root_logger.setLevel(logging.DEBUG)
123
133
 
@@ -166,7 +176,7 @@ class LogConfigurator(object):
166
176
  stevedore_log.setLevel(logging.ERROR)
167
177
  iso8601_log.setLevel(logging.ERROR)
168
178
 
169
- def configure(self, cloud_config):
179
+ def configure(self, cloud_config: cloud_config.CloudConfig) -> None:
170
180
  log_level = log_level_from_config(cloud_config.config)
171
181
  set_warning_filter(log_level)
172
182
  self.dump_trace = cloud_config.config.get('debug', self.dump_trace)
osc_lib/py.typed ADDED
File without changes
osc_lib/shell.py CHANGED
@@ -20,12 +20,15 @@ import getpass
20
20
  import logging
21
21
  import sys
22
22
  import traceback
23
+ import typing as ty
23
24
 
25
+ from cliff import _argparse
24
26
  from cliff import app
25
27
  from cliff import command
26
28
  from cliff import commandmanager
27
29
  from cliff import complete
28
30
  from cliff import help
31
+ from cliff import interactive
29
32
  from oslo_utils import importutils
30
33
  from oslo_utils import strutils
31
34
 
@@ -45,7 +48,7 @@ DEFAULT_DOMAIN = 'default'
45
48
  DEFAULT_INTERFACE = 'public'
46
49
 
47
50
 
48
- def prompt_for_password(prompt=None):
51
+ def prompt_for_password(prompt: ty.Optional[str] = None) -> str:
49
52
  """Prompt user for a password
50
53
 
51
54
  Prompt for a password if stdin is a tty.
@@ -76,26 +79,30 @@ def prompt_for_password(prompt=None):
76
79
  class OpenStackShell(app.App):
77
80
  CONSOLE_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
78
81
 
82
+ client_manager: clientmanager.ClientManager
83
+
79
84
  log = logging.getLogger(__name__)
80
- timing_data = []
85
+ timing_data: list[ty.Any] = []
81
86
 
82
87
  def __init__(
83
88
  self,
84
- description=None,
85
- version=None,
86
- command_manager=None,
87
- stdin=None,
88
- stdout=None,
89
- stderr=None,
90
- interactive_app_factory=None,
91
- deferred_help=False,
92
- ):
89
+ description: ty.Optional[str] = None,
90
+ version: ty.Optional[str] = None,
91
+ command_manager: ty.Optional[commandmanager.CommandManager] = None,
92
+ stdin: ty.Optional[ty.TextIO] = None,
93
+ stdout: ty.Optional[ty.TextIO] = None,
94
+ stderr: ty.Optional[ty.TextIO] = None,
95
+ interactive_app_factory: ty.Optional[
96
+ type['interactive.InteractiveApp']
97
+ ] = None,
98
+ deferred_help: bool = False,
99
+ ) -> None:
93
100
  # Patch command.Command to add a default auth_required = True
94
- command.Command.auth_required = True
101
+ setattr(command.Command, 'auth_required', True)
95
102
 
96
103
  # Some commands do not need authentication
97
- help.HelpCommand.auth_required = False
98
- complete.CompleteCommand.auth_required = False
104
+ setattr(help.HelpCommand, 'auth_required', False)
105
+ setattr(complete.CompleteCommand, 'auth_required', False)
99
106
 
100
107
  # Slight change to the meaning of --debug
101
108
  self.DEFAULT_DEBUG_VALUE = None
@@ -107,7 +114,7 @@ class OpenStackShell(app.App):
107
114
  else:
108
115
  cm = command_manager
109
116
 
110
- super(OpenStackShell, self).__init__(
117
+ super().__init__(
111
118
  description=__doc__.strip(),
112
119
  version=version,
113
120
  command_manager=cm,
@@ -120,21 +127,20 @@ class OpenStackShell(app.App):
120
127
  # Set in subclasses
121
128
  self.api_version = None
122
129
 
123
- self.client_manager = None
124
- self.command_options = None
130
+ self.command_options: list[str] = []
125
131
 
126
132
  self.do_profile = False
127
133
 
128
- def configure_logging(self):
134
+ def configure_logging(self) -> None:
129
135
  """Configure logging for the app."""
130
136
  self.log_configurator = logs.LogConfigurator(self.options)
131
137
  self.dump_stack_trace = self.log_configurator.dump_trace
132
138
 
133
- def run(self, argv):
139
+ def run(self, argv: list[str]) -> int:
134
140
  ret_val = 1
135
141
  self.command_options = argv
136
142
  try:
137
- ret_val = super(OpenStackShell, self).run(argv)
143
+ ret_val = super().run(argv)
138
144
  return ret_val
139
145
  except Exception as e:
140
146
  if not logging.getLogger('').handlers:
@@ -147,14 +153,14 @@ class OpenStackShell(app.App):
147
153
  return ret_val
148
154
 
149
155
  finally:
150
- self.log.info("END return value: %s", ret_val)
156
+ self.log.debug("END return value: %s", ret_val)
151
157
 
152
- def init_profile(self):
158
+ def init_profile(self) -> None:
153
159
  self.do_profile = osprofiler_profiler and self.options.profile
154
160
  if self.do_profile:
155
161
  osprofiler_profiler.init(self.options.profile)
156
162
 
157
- def close_profile(self):
163
+ def close_profile(self) -> None:
158
164
  if self.do_profile:
159
165
  profiler = osprofiler_profiler.get()
160
166
  trace_id = profiler.get_base_id()
@@ -165,35 +171,41 @@ class OpenStackShell(app.App):
165
171
  # printed. In fact we can define custom log level here with value
166
172
  # bigger than most big default one (CRITICAL) or something like
167
173
  # that (PROFILE = 60 for instance), but not sure we need it here.
168
- self.log.warning("Trace ID: %s" % trace_id)
174
+ self.log.warning(f"Trace ID: {trace_id}")
169
175
  self.log.warning(
170
- "Short trace ID "
171
- "for OpenTracing-based drivers: %s" % short_id
176
+ f"Short trace ID for OpenTracing-based drivers: {short_id}"
172
177
  )
173
178
  self.log.warning(
174
179
  "Display trace data with command:\n"
175
- "osprofiler trace show --html %s " % trace_id
180
+ f"osprofiler trace show --html {trace_id} "
176
181
  )
177
182
 
178
- def run_subcommand(self, argv):
183
+ def run_subcommand(self, argv: list[str]) -> int:
179
184
  self.init_profile()
180
185
  try:
181
- ret_value = super(OpenStackShell, self).run_subcommand(argv)
186
+ ret_value = super().run_subcommand(argv)
182
187
  finally:
183
188
  self.close_profile()
184
189
  return ret_value
185
190
 
186
- def interact(self):
191
+ def interact(self) -> None:
187
192
  self.init_profile()
188
193
  try:
189
- ret_value = super(OpenStackShell, self).interact()
194
+ ret_value = super().interact()
190
195
  finally:
191
196
  self.close_profile()
192
197
  return ret_value
193
198
 
194
- def build_option_parser(self, description, version):
195
- parser = super(OpenStackShell, self).build_option_parser(
196
- description, version
199
+ def build_option_parser(
200
+ self,
201
+ description: ty.Optional[str],
202
+ version: ty.Optional[str],
203
+ argparse_kwargs: ty.Optional[dict[str, ty.Any]] = None,
204
+ ) -> _argparse.ArgumentParser:
205
+ parser = super().build_option_parser(
206
+ description,
207
+ version,
208
+ argparse_kwargs,
197
209
  )
198
210
 
199
211
  # service token auth argument
@@ -251,9 +263,7 @@ class OpenStackShell(app.App):
251
263
  metavar='<auth-domain>',
252
264
  dest='default_domain',
253
265
  default=utils.env('OS_DEFAULT_DOMAIN', default=DEFAULT_DOMAIN),
254
- help=_(
255
- 'Default domain ID, default=%s. ' '(Env: OS_DEFAULT_DOMAIN)'
256
- )
266
+ help=_('Default domain ID, default=%s. (Env: OS_DEFAULT_DOMAIN)')
257
267
  % DEFAULT_DOMAIN,
258
268
  )
259
269
  parser.add_argument(
@@ -350,7 +360,6 @@ class OpenStackShell(app.App):
350
360
  )
351
361
 
352
362
  return parser
353
- # return clientmanager.build_plugin_option_parser(parser)
354
363
 
355
364
  """
356
365
  Break up initialize_app() so that overriding it in a subclass does not
@@ -366,7 +375,7 @@ class OpenStackShell(app.App):
366
375
 
367
376
  """
368
377
 
369
- def _final_defaults(self):
378
+ def _final_defaults(self) -> None:
370
379
  # Set the default plugin to None
371
380
  # NOTE(dtroyer): This is here to set up for setting it to a default
372
381
  # in the calling CLI
@@ -393,21 +402,21 @@ class OpenStackShell(app.App):
393
402
  # Save default domain
394
403
  self.default_domain = self.options.default_domain
395
404
 
396
- def _load_plugins(self):
405
+ def _load_plugins(self) -> None:
397
406
  """Load plugins via stevedore
398
407
 
399
408
  osc-lib has no opinion on what plugins should be loaded
400
409
  """
401
410
  pass
402
411
 
403
- def _load_commands(self):
412
+ def _load_commands(self) -> None:
404
413
  """Load commands via cliff/stevedore
405
414
 
406
415
  osc-lib has no opinion on what commands should be loaded
407
416
  """
408
417
  pass
409
418
 
410
- def initialize_app(self, argv):
419
+ def initialize_app(self, argv: list[str]) -> None:
411
420
  """Global app init bits:
412
421
 
413
422
  * set up API versions
@@ -416,10 +425,12 @@ class OpenStackShell(app.App):
416
425
  """
417
426
 
418
427
  # Parent __init__ parses argv into self.options
419
- super(OpenStackShell, self).initialize_app(argv)
428
+ super().initialize_app(argv)
420
429
  self.log.info(
421
430
  "START with options: %s",
422
- strutils.mask_password(" ".join(self.command_options)),
431
+ strutils.mask_password(" ".join(self.command_options))
432
+ if self.command_options
433
+ else "",
423
434
  )
424
435
  self.log.debug("options: %s", strutils.mask_password(self.options))
425
436
 
@@ -435,7 +446,7 @@ class OpenStackShell(app.App):
435
446
  'auth_type': self._auth_type,
436
447
  },
437
448
  )
438
- except (IOError, OSError) as e:
449
+ except OSError as e:
439
450
  self.log.critical("Could not read clouds.yaml configuration file")
440
451
  self.print_help_if_requested()
441
452
  raise e
@@ -476,23 +487,23 @@ class OpenStackShell(app.App):
476
487
  pw_func=prompt_for_password,
477
488
  )
478
489
 
479
- def prepare_to_run_command(self, cmd):
490
+ def prepare_to_run_command(self, cmd: 'command.Command') -> None:
480
491
  """Set up auth and API versions"""
481
- self.log.info(
492
+ self.log.debug(
482
493
  'command: %s -> %s.%s (auth=%s)',
483
494
  getattr(cmd, 'cmd_name', '<none>'),
484
495
  cmd.__class__.__module__,
485
496
  cmd.__class__.__name__,
486
- cmd.auth_required,
497
+ getattr(cmd, 'auth_required', None),
487
498
  )
488
499
 
489
500
  # NOTE(dtroyer): If auth is not required for a command, skip
490
501
  # get_one()'s validation to avoid loading plugins
491
- validate = cmd.auth_required
502
+ validate = getattr(cmd, 'auth_required', False)
492
503
 
493
504
  # NOTE(dtroyer): Save the auth required state of the _current_ command
494
505
  # in the ClientManager
495
- self.client_manager._auth_required = cmd.auth_required
506
+ self.client_manager._auth_required = validate
496
507
 
497
508
  # Validate auth options
498
509
  self.cloud = self.cloud_config.get_one(
@@ -506,26 +517,31 @@ class OpenStackShell(app.App):
506
517
  # Push the updated args into ClientManager
507
518
  self.client_manager._cli_options = self.cloud
508
519
 
509
- if cmd.auth_required:
520
+ if validate:
510
521
  self.client_manager.setup_auth()
511
522
  if hasattr(cmd, 'required_scope') and cmd.required_scope:
512
523
  # let the command decide whether we need a scoped token
513
524
  self.client_manager.validate_scope()
514
525
  # Trigger the Identity client to initialize
515
- self.client_manager.session.auth.auth_ref = (
526
+ self.client_manager.session.auth.auth_ref = ( # type: ignore
516
527
  self.client_manager.auth_ref
517
528
  )
518
529
  return
519
530
 
520
- def clean_up(self, cmd, result, err):
531
+ def clean_up(
532
+ self,
533
+ cmd: 'command.Command',
534
+ result: int,
535
+ err: ty.Optional[BaseException],
536
+ ) -> None:
521
537
  self.log.debug('clean_up %s: %s', cmd.__class__.__name__, err or '')
522
538
 
523
539
  # Close SDK connection if available to have proper cleanup there
524
- if hasattr(self.client_manager, "sdk_connection"):
540
+ if getattr(self.client_manager, "sdk_connection", None) is not None:
525
541
  self.client_manager.sdk_connection.close()
526
542
 
527
543
  # Close session if available
528
- if hasattr(self.client_manager.session, "session"):
544
+ if getattr(self.client_manager.session, "session", None) is not None:
529
545
  self.client_manager.session.session.close()
530
546
 
531
547
  # Process collected timing data
@@ -544,6 +560,7 @@ class OpenStackShell(app.App):
544
560
  # Check the formatter used in the actual command
545
561
  if (
546
562
  hasattr(cmd, 'formatter')
563
+ and hasattr(cmd, '_formatter_plugins')
547
564
  and cmd.formatter != cmd._formatter_plugins['table'].obj
548
565
  ):
549
566
  format = 'csv'
@@ -553,7 +570,7 @@ class OpenStackShell(app.App):
553
570
  tcmd.run(targs)
554
571
 
555
572
 
556
- def main(argv=None):
573
+ def main(argv: ty.Optional[list[str]] = None) -> int:
557
574
  if argv is None:
558
575
  argv = sys.argv[1:]
559
576
  return OpenStackShell().run(argv)
@@ -50,6 +50,6 @@ class TestSession(utils.TestCase):
50
50
  BASE_URL = 'https://api.example.com:1234/test'
51
51
 
52
52
  def setUp(self):
53
- super(TestSession, self).setUp()
53
+ super().setUp()
54
54
  self.sess = session.Session()
55
55
  self.requests_mock = self.useFixture(fixture.Fixture())
@@ -23,7 +23,7 @@ from osc_lib.tests.api import fakes as api_fakes
23
23
 
24
24
  class TestBaseAPIDefault(api_fakes.TestSession):
25
25
  def setUp(self):
26
- super(TestBaseAPIDefault, self).setUp()
26
+ super().setUp()
27
27
  self.api = api.BaseAPI()
28
28
 
29
29
  def test_baseapi_request_no_url(self):
@@ -186,7 +186,7 @@ class TestBaseAPIEndpointArg(api_fakes.TestSession):
186
186
 
187
187
  class TestBaseAPIArgs(api_fakes.TestSession):
188
188
  def setUp(self):
189
- super(TestBaseAPIArgs, self).setUp()
189
+ super().setUp()
190
190
  self.api = api.BaseAPI(
191
191
  session=self.sess,
192
192
  endpoint=self.BASE_URL,
@@ -220,7 +220,7 @@ class TestBaseAPIArgs(api_fakes.TestSession):
220
220
 
221
221
  class TestBaseAPICreate(api_fakes.TestSession):
222
222
  def setUp(self):
223
- super(TestBaseAPICreate, self).setUp()
223
+ super().setUp()
224
224
  self.api = api.BaseAPI(
225
225
  session=self.sess,
226
226
  endpoint=self.BASE_URL,
@@ -258,7 +258,7 @@ class TestBaseAPICreate(api_fakes.TestSession):
258
258
 
259
259
  class TestBaseAPIFind(api_fakes.TestSession):
260
260
  def setUp(self):
261
- super(TestBaseAPIFind, self).setUp()
261
+ super().setUp()
262
262
  self.api = api.BaseAPI(
263
263
  session=self.sess,
264
264
  endpoint=self.BASE_URL,
@@ -452,7 +452,7 @@ class TestBaseAPIFind(api_fakes.TestSession):
452
452
 
453
453
  class TestBaseAPIList(api_fakes.TestSession):
454
454
  def setUp(self):
455
- super(TestBaseAPIList, self).setUp()
455
+ super().setUp()
456
456
  self.api = api.BaseAPI(
457
457
  session=self.sess,
458
458
  endpoint=self.BASE_URL,
@@ -24,7 +24,7 @@ class TestBaseAPIFilter(api_fakes.TestSession):
24
24
  """The filters can be tested independently"""
25
25
 
26
26
  def setUp(self):
27
- super(TestBaseAPIFilter, self).setUp()
27
+ super().setUp()
28
28
  self.api = api.BaseAPI(
29
29
  session=self.sess,
30
30
  endpoint=self.BASE_URL,
@@ -17,7 +17,7 @@ from osc_lib.tests import utils
17
17
 
18
18
  class TestOSCConfig(utils.TestCase):
19
19
  def setUp(self):
20
- super(TestOSCConfig, self).setUp()
20
+ super().setUp()
21
21
 
22
22
  self.cloud = client_config.OSC_Config()
23
23
 
@@ -104,7 +104,7 @@ class TestListDictColumn(utils.TestCase):
104
104
  # OrderedDict is a subclass of dict and would inadvertently pass
105
105
  self.assertEqual(type(col.machine_readable()), list) # noqa: H212
106
106
  for x in col.machine_readable():
107
- self.assertEqual(type(x), dict) # noqa: H212
107
+ self.assertEqual(type(x), dict) # noqa: H211
108
108
 
109
109
 
110
110
  class TestSizeColumn(utils.TestCase):