Repository: softlayer/softlayer-python
Pull request #1785 (2022-10-28 21:15:52)

del-notification commands, rename the commands to notifications-add
[#issue1772](https://github.com/softlayer/softlayer-python/issues/1772)

![image](https://user-images.githubusercontent.com/22535437/198734170-ba428440-6c85-4668-a5e3-d8a0f4e38802.png)



Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ecbd7ad6..2ec9136a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -40,7 +40,7 @@ ## Documentation
 
 Docs are generated with [Sphinx](https://docs.readthedocs.io/en/latest/intro/getting-started-with-sphinx.html) and once Sphinx is setup, you can simply do
 
-`make html` in the softlayer-python/docs directory, which should generate the HTML in `softlayer-python/docs/_build/html` for testing.
+`make html` in the softlayer-python/docs directory, which should generate the HTML in softlayer-python/docs/_build/html for testing. 
 
 
 ## Unit Tests
diff --git a/SoftLayer/CLI/command.py b/SoftLayer/CLI/command.py
index 5b44da1a..95013769 100644
--- a/SoftLayer/CLI/command.py
+++ b/SoftLayer/CLI/command.py
@@ -11,9 +11,11 @@
 import click
 
 from rich import box
+from rich.console import Console
 from rich.highlighter import RegexHighlighter
 from rich.table import Table
 from rich.text import Text
+from rich.theme import Theme
 
 from SoftLayer.CLI import environment
 
@@ -24,42 +26,53 @@ class OptionHighlighter(RegexHighlighter):
         r"(?P<switch>^\-\w)",  # single options like -v
         r"(?P<option>\-\-[\w\-]+)",  # long options like --verbose
         r"(?P<default_option>\[[^\]]+\])",  # anything between [], usually default options
+
     ]
 
 
+#  Colors defined in https://rich.readthedocs.io/en/latest/_modules/rich/color.html#ColorType
+SLCLI_THEME = Theme(
+    {
+        "option": "bold cyan",
+        "switch": "bold green",
+        "default_option": "light_pink1",
+        "option_keyword": "bold cyan",
+        "args_keyword": "bold green"
+    }
+)
+
+
 class CommandLoader(click.MultiCommand):
     """Loads module for click."""
 
     def __init__(self, *path, **attrs):
         click.MultiCommand.__init__(self, **attrs)
         self.path = path
-        self.highlighter = OptionHighlighter()
-        self.env = None
-        self.console = None
 
-    def ensure_env(self, ctx):
-        """ensures self.env is set"""
-        if self.env is None:
-            self.env = ctx.ensure_object(environment.Environment)
-            self.env.load()
-        if self.console is None:
-            self.console = self.env.console
+        self.highlighter = OptionHighlighter()
+        self.console = Console(
+            theme=SLCLI_THEME
+        )
 
     def list_commands(self, ctx):
         """List all sub-commands."""
-        self.ensure_env(ctx)
-        return sorted(self.env.list_commands(*self.path))
+        env = ctx.ensure_object(environment.Environment)
+        env.load()
+
+        return sorted(env.list_commands(*self.path))
 
     def get_command(self, ctx, cmd_name):
         """Get command for click."""
-        self.ensure_env(ctx)
+        env = ctx.ensure_object(environment.Environment)
+        env.load()
+
         # Do alias lookup (only available for root commands)
         if len(self.path) == 0:
-            cmd_name = self.env.resolve_alias(cmd_name)
+            cmd_name = env.resolve_alias(cmd_name)
 
         new_path = list(self.path)
         new_path.append(cmd_name)
-        module = self.env.get_command(*new_path)
+        module = env.get_command(*new_path)
         if isinstance(module, types.ModuleType):
             return CommandLoader(*new_path, help=module.__doc__ or '')
         else:
@@ -67,15 +80,14 @@ def get_command(self, ctx, cmd_name):
 
     def format_usage(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
         """Formats and colorizes the usage information."""
-        self.ensure_env(ctx)
         pieces = self.collect_usage_pieces(ctx)
         for index, piece in enumerate(pieces):
             if piece == "[OPTIONS]":
-                pieces[index] = "[options][OPTIONS][/]"
+                pieces[index] = "[bold cyan][OPTIONS][/bold cyan]"
             elif piece == "COMMAND [ARGS]...":
-                pieces[index] = "[command]COMMAND[/] [args][ARGS][/] ..."
+                pieces[index] = "[orange1]COMMAND[/orange1] [bold cyan][ARGS][/bold cyan] ..."
 
-        self.console.print(f"Usage: [path]{ctx.command_path}[/] {' '.join(pieces)}")
+        self.console.print(f"Usage: [bold red]{ctx.command_path}[/bold red] {' '.join(pieces)}")
 
     def format_help_text(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
         """Writes the help text"""
@@ -141,9 +153,9 @@ def format_commands(self, ctx, formatter):
         if len(commands):
             for subcommand, cmd in commands:
                 help_text = cmd.get_short_help_str(120)
-                command_style = Text(f" {subcommand}", style="sub_command")
+                command_style = Text(f" {subcommand}", style="orange1")
                 command_table.add_row(command_style, help_text)
-        self.console.print("\n[name_sub_command]Commands:[/]")
+        self.console.print("\n[bold orange1]Commands:[/]")
         self.console.print(command_table)
 
 
@@ -153,28 +165,20 @@ class SLCommand(click.Command):
     def __init__(self, **attrs):
         click.Command.__init__(self, **attrs)
         self.highlighter = OptionHighlighter()
-        self.env = None
-        self.console = None
-
-    def ensure_env(self, ctx):
-        """ensures self.env is set"""
-        if self.env is None:
-            self.env = ctx.ensure_object(environment.Environment)
-            self.env.load()
-        if self.console is None:
-            self.console = self.env.console
+        self.console = Console(
+            theme=SLCLI_THEME
+        )
 
     def format_usage(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
         """Formats and colorizes the usage information."""
-        self.ensure_env(ctx)
         pieces = self.collect_usage_pieces(ctx)
         for index, piece in enumerate(pieces):
             if piece == "[OPTIONS]":
-                pieces[index] = "[options][OPTIONS][/]"
+                pieces[index] = "[bold cyan][OPTIONS][/bold cyan]"
             elif piece == "COMMAND [ARGS]...":
-                pieces[index] = "[command]COMMAND[/] [args][ARGS][/] ..."
+                pieces[index] = "[orange1]COMMAND[/orange1] [bold cyan][ARGS][/bold cyan] ..."
 
-        self.console.print(f"Usage: [path]{ctx.command_path}[/] {' '.join(pieces)}")
+        self.console.print(f"Usage: [bold red]{ctx.command_path}[/bold red] {' '.join(pieces)}")
 
     def format_help_text(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
         """Writes the help text"""
@@ -186,7 +190,7 @@ def format_help_text(self, ctx: click.Context, formatter: click.formatting.HelpF
         if text:
             text = inspect.cleandoc(text)
 
-        self.console.print(f"\n\t{text}\n", highlight=False)
+        self.console.print(f"\n\t{text}\n")
 
     def format_epilog(self, ctx: click.Context, formatter: click.formatting.HelpFormatter) -> None:
         """Writes the epilog if it exists, then prints out any sub-commands if they exist."""
diff --git a/SoftLayer/CLI/config/__init__.py b/SoftLayer/CLI/config/__init__.py
index a4b5c578..fc1cca2b 100644
--- a/SoftLayer/CLI/config/__init__.py
+++ b/SoftLayer/CLI/config/__init__.py
@@ -13,7 +13,7 @@ def _resolve_transport(transport):
     return _resolve_transport(nested_transport)
 
 
-def get_settings_from_client(client, theme):
+def get_settings_from_client(client):
     """Pull out settings from a SoftLayer.BaseClient instance.
 
     :param client: SoftLayer.BaseClient instance
@@ -23,7 +23,6 @@ def get_settings_from_client(client, theme):
         'api_key': '',
         'timeout': '',
         'endpoint_url': '',
-        'theme': theme
     }
     try:
         settings['username'] = client.auth.username
@@ -50,5 +49,4 @@ def config_table(settings):
     table.add_row(['API Key', settings['api_key'] or 'not set'])
     table.add_row(['Endpoint URL', settings['endpoint_url'] or 'not set'])
     table.add_row(['Timeout', settings['timeout'] or 'not set'])
-    table.add_row(['Theme', settings['theme'] or 'not set'])
     return table
diff --git a/SoftLayer/CLI/config/setup.py b/SoftLayer/CLI/config/setup.py
index f06825ab..2e2c87b0 100644
--- a/SoftLayer/CLI/config/setup.py
+++ b/SoftLayer/CLI/config/setup.py
@@ -68,7 +68,7 @@ def cli(env, auth):
     api_key = None
 
     timeout = 0
-    defaults = config.get_settings_from_client(env.client, env.theme)
+    defaults = config.get_settings_from_client(env.client)
     endpoint_url = get_endpoint_url(env, defaults.get('endpoint_url', 'public'))
     # Get ths username and API key
     if auth == 'ibmid':
@@ -92,9 +92,6 @@ def cli(env, auth):
     # Ask for timeout, convert to float, then to int
     timeout = int(float(env.input('Timeout', default=defaults['timeout'] or 0)))
 
-    # Ask theme for console
-    theme = env.input('Theme (dark/light)', default=defaults['theme'] or 'dark')
-
     path = '~/.softlayer'
     if env.config_file:
         path = env.config_file
@@ -103,8 +100,7 @@ def cli(env, auth):
     env.out(env.fmt(config.config_table({'username': username,
                                          'api_key': api_key,
                                          'endpoint_url': endpoint_url,
-                                         'timeout': timeout,
-                                         'theme': theme})))
+                                         'timeout': timeout})))
 
     if not formatting.confirm('Are you sure you want to write settings to "%s"?' % config_path, default=True):
         raise exceptions.CLIAbort('Aborted.')
@@ -122,7 +118,6 @@ def cli(env, auth):
     parsed_config.set('softlayer', 'api_key', api_key)
     parsed_config.set('softlayer', 'endpoint_url', endpoint_url)
     parsed_config.set('softlayer', 'timeout', timeout)
-    parsed_config.set('softlayer', 'theme', theme)
 
     config_fd = os.fdopen(os.open(config_path, (os.O_WRONLY | os.O_CREAT | os.O_TRUNC), 0o600), 'w')
     try:
diff --git a/SoftLayer/CLI/config/show.py b/SoftLayer/CLI/config/show.py
index ece6f9e4..a5a933fe 100644
--- a/SoftLayer/CLI/config/show.py
+++ b/SoftLayer/CLI/config/show.py
@@ -13,5 +13,5 @@
 def cli(env):
     """Show current configuration."""
 
-    settings = config.get_settings_from_client(client=env.client, theme=env.theme)
+    settings = config.get_settings_from_client(env.client)
     env.fout(config.config_table(settings))
diff --git a/SoftLayer/CLI/core.py b/SoftLayer/CLI/core.py
index 2f55a0db..b3a1fead 100644
--- a/SoftLayer/CLI/core.py
+++ b/SoftLayer/CLI/core.py
@@ -118,7 +118,6 @@ def cli(env,
     env.skip_confirmations = really
     env.config_file = config
     env.format = format
-    env.set_env_theme(config_file=config)
     env.ensure_client(config_file=config, is_demo=demo, proxy=proxy)
     env.vars['_start'] = time.time()
     logger = logging.getLogger()
diff --git a/SoftLayer/CLI/environment.py b/SoftLayer/CLI/environment.py
index 79964efb..d7b98501 100644
--- a/SoftLayer/CLI/environment.py
+++ b/SoftLayer/CLI/environment.py
@@ -5,9 +5,6 @@
 
     :license: MIT, see LICENSE for more details.
 """
-import configparser
-import os
-
 import importlib
 from json.decoder import JSONDecodeError
 
@@ -19,7 +16,6 @@
 import SoftLayer
 from SoftLayer.CLI import formatting
 from SoftLayer.CLI import routes
-from SoftLayer import utils
 
 # pylint: disable=too-many-instance-attributes, invalid-name
 
@@ -39,8 +35,7 @@ def __init__(self):
         self.vars = {}
 
         self.client = None
-        self.theme = self.set_env_theme()
-        self.console = utils.console_color_themes(self.theme)
+        self.console = Console()
         self.err_console = Console(stderr=True)
         self.format = 'table'
         self.skip_confirmations = False
@@ -81,7 +76,7 @@ def fmt(self, output, fmt=None):
         """Format output based on current the environment format."""
         if fmt is None:
             fmt = self.format
-        return formatting.format_output(output, fmt, self.theme)
+        return formatting.format_output(output, fmt)
 
     def format_output_is_json(self):
         """Return True if format output is json or jsonraw"""
@@ -213,25 +208,6 @@ def ensure_client(self, config_file=None, is_demo=False, proxy=None):
             )
         self.client = client
 
-    def set_env_theme(self, config_file=None):
-        """Get theme to color console and set in env"""
-        theme = os.environ.get('SL_THEME')
-        if theme:
-            return theme
-        else:
-            config_files = ['/etc/softlayer.conf', '~/.softlayer']
-            path_os = os.getenv('HOME')
-            if path_os:
-                config_files.append(path_os + '\\AppData\\Roaming\\softlayer')
-            if config_file:
-                config_files.append(config_file)
-            config = configparser.RawConfigParser({'theme': 'dark'})
-            config.read(config_files)
-            if config.has_section('softlayer'):
-                self.theme = config.get('softlayer', 'theme')
-                return config.get('softlayer', 'theme')
-        return 'dark'
-
 
 class ModuleLoader(object):
     """Module loader that acts a little like an EntryPoint object."""
diff --git a/SoftLayer/CLI/formatting.py b/SoftLayer/CLI/formatting.py
index bf8ce831..b4df8417 100644
--- a/SoftLayer/CLI/formatting.py
+++ b/SoftLayer/CLI/formatting.py
@@ -22,7 +22,7 @@
 FALSE_VALUES = ['0', 'false', 'FALSE', 'no', 'False']
 
 
-def format_output(data, fmt='table', theme=None):  # pylint: disable=R0911,R0912
+def format_output(data, fmt='table'):  # pylint: disable=R0911,R0912
     """Given some data, will format it for console output.
 
     :param data: One of: String, Table, FormattedItem, List, Tuple, SequentialOutput
@@ -40,7 +40,7 @@ def format_output(data, fmt='table', theme=None):  # pylint: disable=R0911,R0912
 
     # responds to .prettytable()
     if hasattr(data, 'prettytable') and fmt in ('table', 'raw'):
-        return format_prettytable(data, fmt, theme)
+        return format_prettytable(data, fmt)
 
     # responds to .to_python()
     if hasattr(data, 'to_python'):
@@ -73,7 +73,7 @@ def format_output(data, fmt='table', theme=None):  # pylint: disable=R0911,R0912
     return str(data)
 
 
-def format_prettytable(table, fmt='table', theme=None):
+def format_prettytable(table, fmt='table'):
     """Converts SoftLayer.CLI.formatting.Table instance to a prettytable."""
     for i, row in enumerate(table.rows):
         for j, item in enumerate(row):
@@ -83,7 +83,7 @@ def format_prettytable(table, fmt='table', theme=None):
                 table.rows[i][j] = format_output(item)
             else:
                 table.rows[i][j] = str(item)
-    ptable = table.prettytable(fmt, theme)
+    ptable = table.prettytable(fmt)
     return ptable
 
 
@@ -267,13 +267,12 @@ def to_python(self):
             items.append(dict(zip(self.columns, formatted_row)))
         return items
 
-    def prettytable(self, fmt='table', theme=None):
+    def prettytable(self, fmt='table'):
         """Returns a RICH table instance."""
         box_style = box.SQUARE
         if fmt == 'raw':
             box_style = None
-        color_table = utils.table_color_theme(theme)
-        table = rTable(title=self.title, box=box_style, header_style=color_table['header'])
+        table = rTable(title=self.title, box=box_style, header_style="bright_cyan")
         if self.sortby:
             try:
                 # https://docs.python.org/3/howto/sorting.html#key-functions
@@ -301,7 +300,7 @@ def prettytable(self, fmt='table', theme=None):
                 justify = 'left'
             # Special coloring for some columns
             if col in ('id', 'Id', 'ID'):
-                style = color_table['id_columns']
+                style = "pale_violet_red1"
             table.add_column(col, justify=justify, style=style)
 
         for row in self.rows:
diff --git a/SoftLayer/CLI/hardware/add_notification.py b/SoftLayer/CLI/hardware/notification_add.py
similarity index 100%
rename from SoftLayer/CLI/hardware/add_notification.py
rename to SoftLayer/CLI/hardware/notification_add.py
diff --git a/SoftLayer/CLI/hardware/notification_delete.py b/SoftLayer/CLI/hardware/notification_delete.py
new file mode 100644
index 00000000..c85f437c
--- /dev/null
+++ b/SoftLayer/CLI/hardware/notification_delete.py
@@ -0,0 +1,21 @@
+"""Remove a user hardware notification entry."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+
+
+@click.command(cls=SoftLayer.CLI.command.SLCommand, )
+@click.argument('identifier')
+@environment.pass_env
+def cli(env, identifier):
+    """Remove a user hardware notification entry."""
+
+    hardware = SoftLayer.HardwareManager(env.client)
+
+    result = hardware.remove_notification(identifier)
+
+    if result:
+        env.fout("The hardware notification instance: {} was deleted.".format(identifier))
diff --git a/SoftLayer/CLI/hardware/notifications.py b/SoftLayer/CLI/hardware/notifications.py
index 76c2bee6..0612dcba 100644
--- a/SoftLayer/CLI/hardware/notifications.py
+++ b/SoftLayer/CLI/hardware/notifications.py
@@ -18,12 +18,13 @@ def cli(env, identifier):
 
     notifications = hardware.get_notifications(identifier)
 
-    table = formatting.KeyValueTable(['Domain', 'Hostmane', 'Username', 'Email', 'FirstName', 'Lastname'])
+    table = formatting.KeyValueTable(['Id', 'Domain', 'Hostmane', 'Username', 'Email', 'FirstName', 'Lastname'])
     table.align['Domain'] = 'r'
     table.align['Username'] = 'l'
 
     for notification in notifications:
-        table.add_row([notification['hardware']['fullyQualifiedDomainName'], notification['hardware']['hostname'],
+        table.add_row([notification['id'],
+                       notification['hardware']['fullyQualifiedDomainName'], notification['hardware']['hostname'],
                        notification['user']['username'], notification['user']['email'],
                        notification['user']['firstName'], notification['user']['lastName']])
 
diff --git a/SoftLayer/CLI/order/preset_list.py b/SoftLayer/CLI/order/preset_list.py
index 92560350..e832cf24 100644
--- a/SoftLayer/CLI/order/preset_list.py
+++ b/SoftLayer/CLI/order/preset_list.py
@@ -8,13 +8,16 @@
 from SoftLayer.CLI import formatting
 from SoftLayer.managers import ordering
 
-COLUMNS = ['Name', 'Key Name', 'Description']
+COLUMNS = ['name',
+           'keyName',
+           'description', ]
 
 
 @click.command(cls=SLCommand)
 @click.argument('package_keyname')
-@click.option('--keyword', help="A word (or string) used to filter preset names.")
-@click.option('--prices', '-p', is_flag=True, help='Use --prices to list the server item prices.')
+@click.option('--keyword',
+              help="A word (or string) used to filter preset names.")
+@click.option('--prices', '-p', is_flag=True, help='Use --prices to list the server item prices, e.g. --prices')
 @environment.pass_env
 def cli(env, package_keyname, keyword, prices):
     """List package presets.
@@ -22,7 +25,6 @@ def cli(env, package_keyname, keyword, prices):
     .. Note::
         Presets are set CPU / RAM / Disk allotments. You still need to specify required items.
         Some packages do not have presets.
-        Cost includes all items in a preset, and may include optional items.
 
     ::
 
@@ -32,37 +34,7 @@ def cli(env, package_keyname, keyword, prices):
         # List the Bare Metal server presets that include a GPU
         slcli order preset-list BARE_METAL_SERVER --keyword gpu
 
-        # Get a specific flavor for Virtual Server
-        slcli order preset-list PUBLIC_CLOUD_SERVER --prices --keyword BL2.56x242x
-
-        # All packages with active presets
-        slcli call-api SoftLayer_Product_Package getAllObjects --mask="mask[id,keyName,activePresetCount]" \
---json-filter='{"activePresets":{"operation":"not null"}}'
-        ┌───────────────────┬──────┬──────────────────────────────────────────────────────────────────┐
-        │ activePresetCount │  id  │                             keyName                              │
-        ├───────────────────┼──────┼──────────────────────────────────────────────────────────────────┤
-        │         1         │ 144  │                              3U_GPU                              │
-        │         6         │ 200  │                        BARE_METAL_SERVER                         │
-        │         1         │ 571  │                           NETWORK_VLAN                           │
-        │        100        │ 835  │                       PUBLIC_CLOUD_SERVER                        │
-        │         6         │ 865  │                     NETWORK_VLAN_FOR_SERVICE                     │
-        │         5         │ 885  │                           8U_BI_S2_H4                            │
-        │        56         │ 991  │                      TRANSIENT_CLOUD_SERVER                      │
-        │        56         │ 1035 │                       SUSPEND_CLOUD_SERVER                       │
-        │         9         │ 1045 │                          2U_BI_S3_H2000                          │
-        │         7         │ 1075 │                   2U_IC4V_FIXED_CONFIGURATIONS                   │
-        │        32         │ 1109 │                           BI_S4_H2000                            │
-        │         8         │ 1117 │                           BI_S4_H4000                            │
-        │         7         │ 1119 │                           BI_S4_H8000                            │
-        │         5         │ 2636 │                    2U_BI_S4_H2000_AEP_ENABLED                    │
-        │         5         │ 2676 │                    4U_BI_S4_H4000_AEP_ENABLED                    │
-        │         5         │ 2700 │                    4U_BI_S4_H8000_AEP_ENABLED                    │
-        │         6         │ 2866 │ ORACLE_APPLICATION_CLUSTER_CASCADE_LAKE_SCALABLE_FAMILY_4_DRIVES │
-        │         1         │ 2874 │          ORACLE_APPLICATION_CLUSTER_COFFEE_LAKE_E2174G           │
-        └───────────────────┴──────┴──────────────────────────────────────────────────────────────────┘
-
     """
-    click.secho("*NOTICE*: Cost includes all items in a preset, and may include optional items.", fg="yellow")
     manager = ordering.OrderingManager(env.client)
 
     _filter = {}
@@ -71,23 +43,19 @@ def cli(env, package_keyname, keyword, prices):
     presets = manager.list_presets(package_keyname, filter=_filter)
 
     if prices:
-        table = formatting.Table(['Id', 'Key Name', 'Hourly', 'Monthly', 'Location'])
-        table.align = {"Id": "center", "Key Name": "left", "Hourly": "right", "Monthly": "right", "Location": "center"}
+        table = formatting.Table(['keyName', 'priceId', 'Hourly', 'Monthly', 'Restriction', 'Location'])
         for price in presets:
-            locations_list = []
+            locations = []
             if price['locations'] != []:
                 for location in price['locations']:
-                    locations_list.append(location['name'])
-                locations = ', '.join(locations_list)
-            else:
-                locations = "All"
-            table.add_row([
-                price['id'],
-                price['keyName'],
-                get_item_price_data(price['prices'], 'hourlyRecurringFee'),
-                get_item_price_data(price['prices'], 'recurringFee'),
-                locations
-            ])
+                    locations.append(location['name'])
+            cr_max = get_item_price_data(price['prices'][0], 'capacityRestrictionMaximum')
+            cr_min = get_item_price_data(price['prices'][0], 'capacityRestrictionMinimum')
+            cr_type = get_item_price_data(price['prices'][0], 'capacityRestrictionType')
+            table.add_row([price['keyName'], price['id'],
+                          get_item_price_data(price['prices'][0], 'hourlyRecurringFee'),
+                          get_item_price_data(price['prices'][0], 'recurringFee'),
+                          "%s - %s %s" % (cr_min, cr_max, cr_type), str(locations)])
 
     else:
         table = formatting.Table(COLUMNS)
@@ -101,10 +69,9 @@ def cli(env, package_keyname, keyword, prices):
     env.fout(table)
 
 
-def get_item_price_data(prices, item_attribute):
+def get_item_price_data(price, item_attribute):
     """Given an SoftLayer_Product_Item_Price, returns its default price data"""
-    result = 0.0
-    for this_price in prices:
-        if item_attribute in this_price:
-            result = result + float(this_price[item_attribute])
-    return round(result, 3)
+    result = '-'
+    if item_attribute in price:
+        result = price[item_attribute]
+    return result
diff --git a/SoftLayer/CLI/routes.py b/SoftLayer/CLI/routes.py
index 6af03600..4acaea0a 100644
--- a/SoftLayer/CLI/routes.py
+++ b/SoftLayer/CLI/routes.py
@@ -55,7 +55,8 @@
     ('virtual:access', 'SoftLayer.CLI.virt.access:cli'),
     ('virtual:monitoring', 'SoftLayer.CLI.virt.monitoring:cli'),
     ('virtual:notifications', 'SoftLayer.CLI.virt.notifications:cli'),
-    ('virtual:add-notification', 'SoftLayer.CLI.virt.add_notification:cli'),
+    ('virtual:notification-add', 'SoftLayer.CLI.virt.notification_add:cli'),
+    ('virtual:notification-delete', 'SoftLayer.CLI.virt.notification_delete:cli'),
 
     ('dedicatedhost', 'SoftLayer.CLI.dedicatedhost'),
     ('dedicatedhost:list', 'SoftLayer.CLI.dedicatedhost.list:cli'),
@@ -295,7 +296,8 @@
     ('hardware:sensor', 'SoftLayer.CLI.hardware.sensor:cli'),
     ('hardware:monitoring', 'SoftLayer.CLI.hardware.monitoring:cli'),
     ('hardware:notifications', 'SoftLayer.CLI.hardware.notifications:cli'),
-    ('hardware:add-notification', 'SoftLayer.CLI.hardware.add_notification:cli'),
+    ('hardware:notification-add', 'SoftLayer.CLI.hardware.notification_add:cli'),
+    ('hardware:notification-delete', 'SoftLayer.CLI.hardware.notification_delete:cli'),
     ('hardware:create-credential', 'SoftLayer.CLI.hardware.create_credential:cli'),
 
     ('securitygroup', 'SoftLayer.CLI.securitygroup'),
diff --git a/SoftLayer/CLI/sshkey/add.py b/SoftLayer/CLI/sshkey/add.py
index 76096a49..93b523cd 100644
--- a/SoftLayer/CLI/sshkey/add.py
+++ b/SoftLayer/CLI/sshkey/add.py
@@ -33,7 +33,7 @@ def cli(env, label, in_file, key, note):
     if key:
         key_text = key
     else:
-        with open(path.expanduser(in_file), encoding="utf-8") as key_file:
+        with open(path.expanduser(in_file), 'rU', encoding="utf-8") as key_file:
             key_text = key_file.read().strip()
             key_file.close()
 
diff --git a/SoftLayer/CLI/virt/detail.py b/SoftLayer/CLI/virt/detail.py
index 022e421a..4c6ddf73 100644
--- a/SoftLayer/CLI/virt/detail.py
+++ b/SoftLayer/CLI/virt/detail.py
@@ -97,9 +97,12 @@ def cli(env, identifier, passwords=False, price=False):
         total_price = utils.lookup(result,
                                    'billingItem',
                                    'nextInvoiceTotalRecurringAmount') or 0
-        if total_price != 0:
-            table.add_row(['Prices', _price_table(utils.lookup(result, 'billingItem'), total_price)])
-            table.add_row(['Price rate', total_price])
+        total_price += sum(p['nextInvoiceTotalRecurringAmount']
+                           for p
+                           in utils.lookup(result,
+                                           'billingItem',
+                                           'children') or [])
+        table.add_row(['price_rate', total_price])
 
     if passwords:
         pass_table = formatting.Table(['software', 'username', 'password'])
@@ -153,17 +156,6 @@ def _bw_table(bw_data):
     return table
 
 
-def _price_table(billing_item, total_price):
-    """Returns an item table"""
-    price_table = formatting.Table(['Item', 'CategoryCode', 'Recurring Price'])
-    price_table.add_row(['Total', '-', total_price])
-    items = billing_item['nextInvoiceChildren']
-    for item in items:
-        if item.get('recurringFee') is not None:
-            price_table.add_row([item['description'], item['categoryCode'], item['recurringFee']])
-    return price_table
-
-
 def _cli_helper_dedicated_host(env, result, table):
     """Get details on dedicated host for a virtual server."""
 
diff --git a/SoftLayer/CLI/virt/add_notification.py b/SoftLayer/CLI/virt/notification_add.py
similarity index 100%
rename from SoftLayer/CLI/virt/add_notification.py
rename to SoftLayer/CLI/virt/notification_add.py
diff --git a/SoftLayer/CLI/virt/notification_delete.py b/SoftLayer/CLI/virt/notification_delete.py
new file mode 100644
index 00000000..ed466339
--- /dev/null
+++ b/SoftLayer/CLI/virt/notification_delete.py
@@ -0,0 +1,21 @@
+"""Remove a user VS notification entry."""
+# :license: MIT, see LICENSE for more details.
+
+import click
+
+import SoftLayer
+from SoftLayer.CLI import environment
+
+
+@click.command(cls=SoftLayer.CLI.command.SLCommand, )
+@click.argument('identifier')
+@environment.pass_env
+def cli(env, identifier):
+    """Remove a user VS notification entry."""
+
+    virtual = SoftLayer.VSManager(env.client)
+
+    result = virtual.remove_notification(identifier)
+
+    if result:
+        env.fout("The virtual server notification instance: {} was deleted.".format(identifier))
diff --git a/SoftLayer/CLI/virt/notifications.py b/SoftLayer/CLI/virt/notifications.py
index 103c5695..d836b4ac 100644
--- a/SoftLayer/CLI/virt/notifications.py
+++ b/SoftLayer/CLI/virt/notifications.py
@@ -18,12 +18,13 @@ def cli(env, identifier):
 
     notifications = virtual.get_notifications(identifier)
 
-    table = formatting.KeyValueTable(['Domain', 'Hostmane', 'Username', 'Email', 'FirstName', 'Lastname'])
+    table = formatting.KeyValueTable(['Id', 'Domain', 'Hostmane', 'Username', 'Email', 'FirstName', 'Lastname'])
     table.align['Domain'] = 'r'
     table.align['Username'] = 'l'
 
     for notification in notifications:
-        table.add_row([notification['guest']['fullyQualifiedDomainName'], notification['guest']['hostname'],
+        table.add_row([notification['id'],
+                       notification['guest']['fullyQualifiedDomainName'], notification['guest']['hostname'],
                        notification['user']['username'], notification['user']['email'],
                        notification['user']['lastName'], notification['user']['firstName']])
 
diff --git a/SoftLayer/fixtures/SoftLayer_User_Customer_Notification_Hardware.py b/SoftLayer/fixtures/SoftLayer_User_Customer_Notification_Hardware.py
index 1441b858..c0fa6810 100644
--- a/SoftLayer/fixtures/SoftLayer_User_Customer_Notification_Hardware.py
+++ b/SoftLayer/fixtures/SoftLayer_User_Customer_Notification_Hardware.py
@@ -68,3 +68,5 @@
 
     }
 }
+
+deleteObjects = True
diff --git a/SoftLayer/fixtures/SoftLayer_User_Customer_Notification_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_User_Customer_Notification_Virtual_Guest.py
index 0164c5a2..886e5ffb 100644
--- a/SoftLayer/fixtures/SoftLayer_User_Customer_Notification_Virtual_Guest.py
+++ b/SoftLayer/fixtures/SoftLayer_User_Customer_Notification_Virtual_Guest.py
@@ -53,3 +53,5 @@
         'username': 'test',
     }
 }
+
+deleteObjects = True
diff --git a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
index 6ca56d77..aab22b5e 100644
--- a/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
+++ b/SoftLayer/fixtures/SoftLayer_Virtual_Guest.py
@@ -34,60 +34,6 @@
                 'nextInvoiceTotalRecurringAmount': 1
             }
         ],
-        "nextInvoiceChildren": [
-            {
-                "allowCancellationFlag": 1,
-                "associatedBillingItemId": "738974962",
-                "cancellationDate": "2022-11-03T23:00:01-06:00",
-                "categoryCode": "os_usage",
-                "createDate": "2022-10-04T17:00:08-06:00",
-                "cycleStartDate": "2022-10-04T17:00:08-06:00",
-                "description": "CentOS 8.x Usage is 743.9997 hours Used",
-                "id": 999303609,
-                "laborFee": "0",
-                "laborFeeTaxRate": "0",
-                "lastBillDate": None,
-                "modifyDate": None,
-                "nextBillDate": "2022-11-03T23:00:00-06:00",
-                "oneTimeFee": "0",
-                "oneTimeFeeTaxRate": "0",
-                "orderItemId": None,
-                "parentId": None,
-                "recurringFee": "0",
-                "recurringFeeTaxRate": "0",
-                "recurringMonths": 1,
-                "serviceProviderId": 1,
-                "setupFee": "0",
-                "setupFeeTaxRate": "0",
-                "associatedBillingItem": None
-            },
-            {
-                "allowCancellationFlag": 1,
-                "associatedBillingItemId": "738974962",
-                "cancellationDate": "2022-11-03T23:00:01-06:00",
-                "categoryCode": "guest_core_usage",
-                "createDate": "2022-10-04T17:00:09-06:00",
-                "cycleStartDate": "2022-10-04T17:00:09-06:00",
-                "description": "CPU Cores: a suspendable product.",
-                "id": 999303621,
-                "laborFee": "0",
-                "laborFeeTaxRate": "0",
-                "lastBillDate": None,
-                "modifyDate": None,
-                "nextBillDate": "2022-11-03T23:00:00-06:00",
-                "oneTimeFee": "0",
-                "oneTimeFeeTaxRate": "0",
-                "orderItemId": None,
-                "parentId": None,
-                "recurringFee": "1.54",
-                "recurringFeeTaxRate": "0",
-                "recurringMonths": 1,
-                "serviceProviderId": 1,
-                "setupFee": "0",
-                "setupFeeTaxRate": "0",
-                "associatedBillingItem": None
-            },
-        ],
         'package': {
             "id": 835,
             "keyName": "PUBLIC_CLOUD_SERVER"
diff --git a/SoftLayer/managers/hardware.py b/SoftLayer/managers/hardware.py
index 88ecb7bb..4d3a27be 100644
--- a/SoftLayer/managers/hardware.py
+++ b/SoftLayer/managers/hardware.py
@@ -1110,6 +1110,12 @@ def create_credential(self, template):
         """Create a password for a software component"""
         return self.client.call('SoftLayer_Software_Component_Password', 'createObject', template)
 
+    def remove_notification(self, identifier):
+        """Remove a user hardware notification entry"""
+
+        template = [{'id': identifier}]
+        return self.client.call('SoftLayer_User_Customer_Notification_Hardware', 'deleteObjects', template)
+
 
 def _get_bandwidth_key(items, hourly=True, no_public=False, location=None):
     """Picks a valid Bandwidth Item, returns the KeyName"""
diff --git a/SoftLayer/managers/ordering.py b/SoftLayer/managers/ordering.py
index 23137507..bfa33731 100644
--- a/SoftLayer/managers/ordering.py
+++ b/SoftLayer/managers/ordering.py
@@ -16,7 +16,7 @@
 
 PACKAGE_MASK = '''id, name, keyName, isActive, type'''
 
-PRESET_MASK = '''id, name, keyName, description, prices[id, hourlyRecurringFee, recurringFee], locations'''
+PRESET_MASK = '''id, name, keyName, description, categories, prices, locations'''
 
 
 class OrderingManager(object):
diff --git a/SoftLayer/managers/vs.py b/SoftLayer/managers/vs.py
index 90cf4bf4..acfbec02 100644
--- a/SoftLayer/managers/vs.py
+++ b/SoftLayer/managers/vs.py
@@ -246,10 +246,6 @@ def get_instance(self, instance_id, **kwargs):
                 '''billingItem[id,nextInvoiceTotalRecurringAmount,
                                package[id,keyName],
                                children[description,categoryCode,nextInvoiceTotalRecurringAmount],
-                               nextInvoiceChildren[description,
-                                                   categoryCode,
-                                                   recurringFee,
-                                                   nextInvoiceTotalRecurringAmount],
                                orderItem[id,
                                          order.userRecord[username],
                                          preset.keyName]],'''
@@ -1474,3 +1470,9 @@ def add_notification(self, virtual_id, user_id):
         mask = 'user'
         return self.client.call('SoftLayer_User_Customer_Notification_Virtual_Guest',
                                 'createObject', template, mask=mask)
+
+    def remove_notification(self, identifier):
+        """Remove a user vs notification entry"""
+
+        template = [{'id': identifier}]
+        return self.client.call('SoftLayer_User_Customer_Notification_Virtual_Guest', 'deleteObjects', template)
diff --git a/SoftLayer/transports/soap.py.unstable b/SoftLayer/transports/soap.py
similarity index 99%
rename from SoftLayer/transports/soap.py.unstable
rename to SoftLayer/transports/soap.py
index 9dfbcffa..d604ec70 100644
--- a/SoftLayer/transports/soap.py.unstable
+++ b/SoftLayer/transports/soap.py
@@ -5,7 +5,6 @@
 
     :license: MIT, see LICENSE for more details.
 """
-
 import logging
 
 # Try to import zeep, make sure its softlayer_zeep, error otherwise
diff --git a/SoftLayer/utils.py b/SoftLayer/utils.py
index df6d3438..54502d41 100644
--- a/SoftLayer/utils.py
+++ b/SoftLayer/utils.py
@@ -11,8 +11,6 @@
 import re
 import time
 
-from rich.console import Console
-from rich.theme import Theme
 
 # pylint: disable=no-member, invalid-name
 
@@ -461,50 +459,3 @@ def decode_stacked(document, pos=0, decoder=JSONDecoder()):
         obj, pos = decoder.raw_decode(document, pos)
 
         yield obj
-
-
-def console_color_themes(theme):
-    """Colors in https://rich.readthedocs.io/en/stable/appendix/colors.html?highlight=light_pink1#standard-colors"""
-
-    if theme == 'light':
-        return Console(theme=Theme(
-            {
-                "options": "bold dark_cyan",  # OPTIONS
-                "command": "orange3",  # COMMAND
-                "args": "bold dark_cyan",  # ARGS
-                "path": "bold red",  # command path
-                "name_sub_command": "orange3",  # sub command name
-                "sub_command": "orange3",  # sub command list
-                # Help table colors options
-                "option": "bold dark_cyan",
-                "switch": "bold green4",
-                "default_option": "light_coral",
-                "option_keyword": "bold dark_cyan",
-                "args_keyword": "bold green4",
-            })
-        )
-    return Console(theme=Theme(
-        {
-            "options": "bold cyan",  # OPTIONS
-            "command": "orange3",  # COMMAND
-            "args": "bold cyan",  # ARGS
-            "path": "bold red",  # command path
-            "name_sub_command": "orange3",  # sub command name
-            "sub_command": "orange3",  # sub command list
-            # Help table colors options
-            "option": "bold cyan",
-            "switch": "bold green",
-            "default_option": "light_pink1",
-            "option_keyword": "bold cyan",
-            "args_keyword": "bold green",
-        })
-    )
-
-
-def table_color_theme(theme):
-    """Define result table colors"""
-    if theme == 'light':
-        return {'header': 'dark_cyan',
-                'id_columns': 'light_coral'}
-    return {'header': 'bright_cyan',
-            'id_columns': 'pale_violet_red1'}
diff --git a/docs/cli/hardware.rst b/docs/cli/hardware.rst
index 5689b5e4..490edade 100644
--- a/docs/cli/hardware.rst
+++ b/docs/cli/hardware.rst
@@ -136,8 +136,12 @@ This function updates the firmware of a server. If already at the latest version
    :prog: hardware notifications
    :show-nested:
 
-.. click:: SoftLayer.CLI.hardware.add_notification:cli
-   :prog: hardware add-notification
+.. click:: SoftLayer.CLI.hardware.notification_add:cli
+   :prog: hardware notification-add
+   :show-nested:
+
+.. click:: SoftLayer.CLI.hardware.notification_delete:cli
+   :prog: hardware notification-delete
    :show-nested:
 
 .. click:: SoftLayer.CLI.hardware.create_credential:cli
diff --git a/docs/cli/vs.rst b/docs/cli/vs.rst
index f8143f4e..d9fb3b2b 100644
--- a/docs/cli/vs.rst
+++ b/docs/cli/vs.rst
@@ -283,8 +283,12 @@ If no timezone is specified, IMS local time (CST) will be assumed, which might n
    :prog: virtual notifications
    :show-nested:
 
-.. click:: SoftLayer.CLI.virt.add_notification:cli
-   :prog: virtual add-notification
+.. click:: SoftLayer.CLI.virt.notification_add:cli
+   :prog: virtual notification-add
+   :show-nested:
+
+.. click:: SoftLayer.CLI.virt.notification_delete:cli
+   :prog: virtual notification-delete
    :show-nested:
 
 Manages the migration of virutal guests. Supports migrating virtual guests on Dedicated Hosts as well.
diff --git a/tox.ini b/tox.ini
index 084231ff..9301a2c7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py37,py38,py39,py310,py311,pypy3,analysis,coverage,docs
+envlist = py37,py38,py39,py310,pypy3,analysis,coverage,docs
 
 
 [flake8]


Test patch:
diff --git a/tests/CLI/modules/config_tests.py b/tests/CLI/modules/config_tests.py
index 5ee79406..ef16edf3 100644
--- a/tests/CLI/modules/config_tests.py
+++ b/tests/CLI/modules/config_tests.py
@@ -30,13 +30,13 @@ def set_up(self):
 
     def test_show(self):
         result = self.run_command(['config', 'show'])
+
         self.assert_no_fail(result)
         self.assertEqual(json.loads(result.output),
                          {'Username': 'username',
                           'API Key': 'api-key',
                           'Endpoint URL': 'http://endpoint-url',
-                          'Timeout': 'not set',
-                          'Theme': 'dark'})
+                          'Timeout': 'not set'})
 
 
 class TestHelpSetup(testing.TestCase):
@@ -63,12 +63,12 @@ def tearDown(self):
     @mock.patch('SoftLayer.CLI.environment.Environment.input')
     def test_setup(self, mocked_input, getpass, confirm_mock, client):
         client.return_value = self.env.client
-        if (sys.platform.startswith("win")):
+        if(sys.platform.startswith("win")):
             self.skipTest("Test doesn't work in Windows")
         with tempfile.NamedTemporaryFile() as config_file:
             confirm_mock.return_value = True
             getpass.return_value = 'A' * 64
-            mocked_input.side_effect = ['public', 'user', 0, 'dark']
+            mocked_input.side_effect = ['public', 'user', 0]
 
             result = self.run_command(['--config=%s' % config_file.name, 'config', 'setup'])
 
@@ -80,7 +80,6 @@ def test_setup(self, mocked_input, getpass, confirm_mock, client):
             self.assertIn('username = user', contents)
             self.assertIn('api_key = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', contents)
             self.assertIn('endpoint_url = %s' % consts.API_PUBLIC_ENDPOINT, contents)
-            self.assertIn('theme = dark', contents)
 
     @mock.patch('SoftLayer.Client')
     @mock.patch('SoftLayer.CLI.formatting.confirm')
@@ -90,7 +89,7 @@ def test_setup_float_timeout(self, mocked_input, getpass, confirm_mock, client):
         client.return_value = self.env.client
         confirm_mock.return_value = True
         getpass.return_value = 'A' * 64
-        mocked_input.side_effect = ['public', 'user', 10.0, 'None']
+        mocked_input.side_effect = ['public', 'user', 10.0]
 
         result = self.run_command(['--config=%s' % self.config_file, 'config', 'setup'])
 
@@ -115,7 +114,8 @@ def test_setup_cancel(self, mocked_input, getpass, confirm_mock, client):
         with tempfile.NamedTemporaryFile() as config_file:
             confirm_mock.return_value = False
             getpass.return_value = 'A' * 64
-            mocked_input.side_effect = ['public', 'user', 0, 'None']
+            mocked_input.side_effect = ['public', 'user', 0]
+
             result = self.run_command(['--config=%s' % config_file.name, 'config', 'setup'])
             self.assertEqual(result.exit_code, 2)
             self.assertIsInstance(result.exception, exceptions.CLIAbort)
diff --git a/tests/CLI/modules/order_tests.py b/tests/CLI/modules/order_tests.py
index 88c5d869..246aa920 100644
--- a/tests/CLI/modules/order_tests.py
+++ b/tests/CLI/modules/order_tests.py
@@ -265,11 +265,15 @@ def test_verify_monthly(self):
                          json.loads(result.output))
 
     def test_preset_list(self):
-        active_preset1 = {'name': 'active1', 'keyName': 'PRESET1', 'description': 'description1'}
-        active_preset2 = {'name': 'active2', 'keyName': 'PRESET2', 'description': 'description2'}
-        acc_preset = {'name': 'account1', 'keyName': 'PRESET3', 'description': 'description3'}
+        active_preset1 = {'name': 'active1', 'keyName': 'PRESET1',
+                          'description': 'description1'}
+        active_preset2 = {'name': 'active2', 'keyName': 'PRESET2',
+                          'description': 'description2'}
+        acc_preset = {'name': 'account1', 'keyName': 'PRESET3',
+                      'description': 'description3'}
         active_mock = self.set_mock('SoftLayer_Product_Package', 'getActivePresets')
-        account_mock = self.set_mock('SoftLayer_Product_Package', 'getAccountRestrictedActivePresets')
+        account_mock = self.set_mock('SoftLayer_Product_Package',
+                                     'getAccountRestrictedActivePresets')
         active_mock.return_value = [active_preset1, active_preset2]
         account_mock.return_value = [acc_preset]
 
@@ -278,9 +282,16 @@ def test_preset_list(self):
         self.assert_no_fail(result)
         self.assert_called_with('SoftLayer_Product_Package', 'getActivePresets')
         self.assert_called_with('SoftLayer_Product_Package', 'getAccountRestrictedActivePresets')
-        self.assertIn('"Key Name": "PRESET1",', result.output)
-        self.assertIn('"Description": "description2"', result.output)
-        self.assertIn('"Name": "account1",', result.output)
+        self.assertEqual([{'name': 'active1',
+                           'keyName': 'PRESET1',
+                           'description': 'description1'},
+                          {'name': 'active2',
+                           'keyName': 'PRESET2',
+                           'description': 'description2'},
+                          {'name': 'account1',
+                           'keyName': 'PRESET3',
+                           'description': 'description3'}],
+                         json.loads(result.output))
 
     def test_preset_list_keywork(self):
         result = self.run_command(['order', 'preset-list', 'package', '--keyword', 'testKeyWord'])
@@ -289,20 +300,9 @@ def test_preset_list_keywork(self):
         self.assert_called_with('SoftLayer_Product_Package', 'getActivePresets', filter=_filter)
 
     def test_preset_list_prices(self):
-        active_preset1 = {
-            'name': 'active1', 'keyName': 'PRESET1', 'description': 'description1', 'locations': [], 'id': 118822,
-            'prices': [
-                {'hourlyRecurringFee': 100, 'recurringFee': 55},
-                {'hourlyRecurringFee': 110, 'recurringFee': 44},
-            ]
-        }
-        active_mock = self.set_mock('SoftLayer_Product_Package', 'getActivePresets')
-        active_mock.return_value = [active_preset1]
         result = self.run_command(['order', 'preset-list', 'package', '--prices'])
         self.assert_no_fail(result)
         self.assert_called_with('SoftLayer_Product_Package', 'getActivePresets')
-        self.assertIn("210", result.output)
-        self.assertIn("99", result.output)
 
     def test_location_list(self):
         result = self.run_command(['order', 'package-locations', 'package'])
diff --git a/tests/CLI/modules/server_tests.py b/tests/CLI/modules/server_tests.py
index 7de8fa05..1d0109e8 100644
--- a/tests/CLI/modules/server_tests.py
+++ b/tests/CLI/modules/server_tests.py
@@ -1045,7 +1045,11 @@ def test_notifications(self):
         self.assert_no_fail(result)
 
     def test_add_notification(self):
-        result = self.run_command(['hardware', 'add-notification', '100', '--users', '123456'])
+        result = self.run_command(['hardware', 'notification-add', '100', '--users', '123456'])
+        self.assert_no_fail(result)
+
+    def test_notification_delete(self):
+        result = self.run_command(['hardware', 'notification-delete', '100'])
         self.assert_no_fail(result)
 
     def test_create_credential(self):
diff --git a/tests/CLI/modules/vs/vs_tests.py b/tests/CLI/modules/vs/vs_tests.py
index 13a04fa6..43e66084 100644
--- a/tests/CLI/modules/vs/vs_tests.py
+++ b/tests/CLI/modules/vs/vs_tests.py
@@ -167,7 +167,7 @@ def test_detail_vs(self):
         self.assert_no_fail(result)
         output = json.loads(result.output)
         self.assertEqual(output['notes'], 'notes')
-        self.assertEqual(output['Price rate'], 1.54)
+        self.assertEqual(output['price_rate'], 6.54)
         self.assertEqual(output['users'][0]['username'], 'user')
         self.assertEqual(output['vlans'][0]['number'], 23)
         self.assertEqual(output['owner'], 'chechu')
@@ -984,5 +984,9 @@ def test_notifications(self):
         self.assert_no_fail(result)
 
     def test_add_notification(self):
-        result = self.run_command(['vs', 'add-notification', '100', '--users', '123456'])
+        result = self.run_command(['vs', 'notification-add', '100', '--users', '123456'])
+        self.assert_no_fail(result)
+
+    def test_notification_delete(self):
+        result = self.run_command(['vs', 'notification-delete', '100'])
         self.assert_no_fail(result)
diff --git a/tests/managers/hardware_tests.py b/tests/managers/hardware_tests.py
index 2d324d9d..e8f70840 100644
--- a/tests/managers/hardware_tests.py
+++ b/tests/managers/hardware_tests.py
@@ -928,6 +928,10 @@ def test_add_notification(self):
         self.hardware.add_notification(100, 123456)
         self.assert_called_with('SoftLayer_User_Customer_Notification_Hardware', 'createObject')
 
+    def test_notification_del(self):
+        self.hardware.remove_notification(100)
+        self.assert_called_with('SoftLayer_User_Customer_Notification_Hardware', 'deleteObjects')
+
     def test_get_software_component(self):
         self.hardware.get_software_components(123456)
         self.assert_called_with('SoftLayer_Hardware', 'getSoftwareComponents')
diff --git a/tests/managers/vs/vs_tests.py b/tests/managers/vs/vs_tests.py
index 8bf2fe8e..74bfbc1a 100644
--- a/tests/managers/vs/vs_tests.py
+++ b/tests/managers/vs/vs_tests.py
@@ -1338,3 +1338,7 @@ def test_notification(self):
     def test_add_notification(self):
         self.vs.add_notification(100, 123456)
         self.assert_called_with('SoftLayer_User_Customer_Notification_Virtual_Guest', 'createObject')
+
+    def test_notification_del(self):
+        self.vs.remove_notification(100)
+        self.assert_called_with('SoftLayer_User_Customer_Notification_Virtual_Guest', 'deleteObjects')
diff --git a/tests/transports/soap_tests.py.unstable b/tests/transports/soap_tests.py
similarity index 95%
rename from tests/transports/soap_tests.py.unstable
rename to tests/transports/soap_tests.py
index c80c692c..e7ac4f61 100644
--- a/tests/transports/soap_tests.py.unstable
+++ b/tests/transports/soap_tests.py
@@ -4,11 +4,6 @@
 
     :license: MIT, see LICENSE for more details.
 """
-
-"""
-Commented out for now, py3.11 has some issues with lxml and softlayer-zeep.
-Since no one really uses the soap transport, we will just look at this later.
-
 import io
 import os
 import requests
@@ -32,7 +27,7 @@ def setup_response(filename, status_code=200, total_items=1):
     response.status_code = status_code
     return response
 
-@pytest.mark.skip(reason="no way of currently testing this")
+
 class TestSoapAPICall(testing.TestCase):
 
     def set_up(self):
@@ -119,4 +114,3 @@ def test_virtualGuest(self, zeep_post):
             self.assertEqual(thisVsi.get('id'), vsi.get('id'))
 
     # NEXT MORE COMPLEX OBJECT FILTERS!
-"""
diff --git a/tools/test-requirements.txt b/tools/test-requirements.txt
index 6b546ccf..c009e0ce 100644
--- a/tools/test-requirements.txt
+++ b/tools/test-requirements.txt
@@ -11,4 +11,4 @@ prompt_toolkit >= 2
 pygments >= 2.0.0
 urllib3 >= 1.24
 rich >= 12.3.0
-# softlayer-zeep >= 5.0.0
+softlayer-zeep >= 5.0.0