Repository: QualiSystems/cloudshell-cli

Issue:
Add deprecated import for CLI
cloudshell.cli.service.cli -> cloudshell.cli.cli

Discussion:

Patch:
diff --git a/.travis.yml b/.travis.yml
index 2166a98..9b5ce21 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,5 @@
 language: python
-
+python: 3.7
 jobs:
   include:
     - if: branch = master
@@ -7,7 +7,6 @@ jobs:
       env: TOXENV=py27-master
       after_success: codecov
     - if: branch = master
-      python: 3.7
       env: TOXENV=py37-master
       after_success: codecov
     - if: branch != master
@@ -15,16 +14,76 @@ jobs:
       env: TOXENV=py27-dev
       after_success: codecov
     - if: branch != master
-      python: 3.7
       env: TOXENV=py37-dev
       after_success: codecov
     - env: TOXENV=build
-      python: 2.7
     - env: TOXENV=pre-commit
-      python: 3.7
+    - stage: Deploy to Test PyPI release
+      env: TOXENV=build
+      before_script: sed -i -E "s/^([0-9]+\.[0-9]+\.[0-9]+)$/\1.$TRAVIS_BUILD_NUMBER/" version.txt
+      deploy:
+        distributions: skip
+        skip_cleanup: true
+        provider: pypi
+        server: https://test.pypi.org/legacy/
+        user: "__token__"
+        password:
+          secure: "MueXAWCLVldixZjac8QCbR2MW6l5fkXrUx3xrd4yWX3GN+SPMapnvO7KBhXOskGtgX9hEYeP1NkzkFiLcL4WvGYg+6BtKQO3MHgKEwgSoFq/Zb/cm9KP0kBhaMm06Xk+0B2HR7NjV/F2l4vlmaAw5jwYvbq4T8PATRXfeCYFcuy/HD6S2OldKvNMFDInpVrrgDki6jNK/Nxf1go5e225sdj/4xOU7ebRidDkiWGVIGSIdOh9ZZ2DN1uIhg+F14HwZAe+ebfYugBuNkBl4LqQi++rue36w5d7/W0cHlFeTHB2f26ncBPaExQJynCUn7aq+H/7JLRcv+ZmmK14rSQjk753n41749KmtVh0VK53D84oSOaeWYa3LgXYCqZJNO0QjS/PVKfvgrkU8BM77e1Pl4XYQxQt2BsxJqvzchbqzCno/UX2QJ+wpUGKUtwn3hoMKKJYYhI8e5YYJzzN1zcr85OkmrMzyAqNXYyAZnw6qneX+45N9Xs07j+WsnhZt+tTo72udpksV1MiOPY9K/wYSmiP1OttTohPxhaSNK3UOqyZ7BWrkugt+iovoDLWW+15D2Q5FdXw4pCnVlr/FqPJ27QfSC6t46JozqwTpY68e0FztfAkM7mJZiUyPN+g5mOq3/J3vP5OmYFNlpBDQdcNo/a7OlwTODhTaGej8dt97lg="
+        on:
+          branch: dev
+    - stage: Deploy to PyPI release
+      env: TOXENV=build
+      deploy:
+        distributions: skip
+        skip_cleanup: true
+        provider: pypi
+        user: "__token__"
+        password:
+          secure: "dGuH2O4QDI+TKBGkxcWq9eSAAHHniaWPkW7OyFRf6786gBeiLTsHPxs1V0LhhKu2HLhniBqv11y0JEnbXpq32va+E5ImpDN03W+zGilq50cDiAzE8ihHRH8g4h7J4IJD/Hg6zriAkZY5CM7BwEFa3apuKbrnbsTXb8+ygSAlc+59lNjxzMd7rP4gC1fs5nkF/NLsGPH7zvcurs5JvT4inIFejiFDvqRa3H+6JMADI3Y+LCSDvD9eoH+dHrok38NURKAOcS6mfRAOBZEDhUpjG3DLm79G1BZNOv9O9fhzGNeNC49/TnLjLBAecMWlRqQhsCUEtypKoMtkEuJJoYgWn8dTPcW+FSo1a+NAnDi7jAB0ULqETzRD4vZNGH95rzJgi22mLms3Q3qQnfyr5PALXm42ikJpqXaGNhbWJBDWdTixHI+eJjuoGIWQXHoLmwsa3kVSKlTB/r2fsTfEQMYXky8ecZdUYiBBa9TqDX8cu2XDP/sn+VM4/kLa/wzsHe0gbO+m8XTcloPwYAy2UIjumLHxeJcyrDilwlmy9v2jLzN58juJZAwQrJyQMB6OZ5yAT5XFPKYgdM3AheWVRZytd30YUVsw9UlPzJPS5ZxyiDd94yZvOJzrp8VkKZV9M8bIkezad+zJRw59d/bP51Ot8Qx27JMXb9KeBZ7RD/obnRk="
+        on:
+          tags: true
+    - stage: Create GitHub release
+      env: TOXENV=build
+      before_deploy:
+        - export AUTHOR_EMAIL="$(git log -1 $TRAVIS_COMMIT --pretty="%cE")"
+        - export AUTHOR_NAME="$(git log -1 $TRAVIS_COMMIT --pretty="%aN")"
+        - export GIT_TAG="$(cat version.txt | tr -d ' \t\n\r')"
+        - git config --local user.name $AUTHOR_NAME
+        - git config --local user.email $AUTHOR_EMAIL
+        - git tag $GIT_TAG
+      deploy:
+        provider: releases
+        skip_cleanup: true
+        draft: true
+        api_key:
+          secure: "Slv+t+YmXb8uA36XU1MQ6vwk+5Ta/XtmQZHyNHGqUg7hTVgqDZyx6dr+RspLZ3sVyO2T9VDb6R+V9Rl/HPUxkdAX/WjSgXH3X3M+ITNKA7KMSX9BOdWKDe+VtqWyUqdCpjfOyiOwr8HE/G/1qXlqNn8CZOb0MKM2VQ5AMPK3FnAnQEKoeFXcD9op+R5cLxeKH6ujkHPFrB0mJrag/b9kgZfP/WSVKNNqLZQaF1gw5s47wFAAau3p7ADc7ujtMaeeBmgbpKBESPPrRBzYSoZpSZ8dXcVeazcWwAIwjFHnbDOPXsa+xGbqBcCrknmFMAEJTakvSjr3eNLr4u67EQ3OeFpHBUuoZ+8MXPA9dRt76g9J0Ukch9NwBE4kaZKlp+tP1HlkB50FO9RCy7hgl6FQ1UFKwNUDIlBaACHX2tQZF6pvf8pkAduDECyOixw2txzBdhBxJYWlAMMOEudwElKvsRyt/3K07phy/xYSpqWvZN7hW7h3/Wd5PkaNfNQtGsb+ifT1KWbiV9uXKHLosjmUD0Tm8kWTIyerqOFzS3Q9bh7G0FSElLIRWMEeVKF/lQdXy+1Nd+DupG6nXrUz6HpJRm6FKWkbC3FF7WOQwRlTrvSVR5HJjjeXDYykjs+J5Bod3EqeyC4yr2axVwS0LEqyIjX0RFTk6gO/UEXmKvCv/vs="
+        file_glob: true
+        file: dist/*
+        name: cloudshell-cli $GIT_TAG
+        target_commitish: master
+        on:
+          branch: master
+    - stage: Check version
+      language: bash
+      install:
+        - git clone https://github.com/$TRAVIS_REPO_SLUG.git $TRAVIS_REPO_SLUG
+        - cd $TRAVIS_REPO_SLUG
+        - git checkout -qf $TRAVIS_PULL_REQUEST_BRANCH
+      script: "! git diff --exit-code --quiet origin/master version.txt"
 
 install:
   - pip install tox
   - pip install codecov
 
 script: tox
+
+stages:
+  - name: Check version
+    if: branch = master AND type = pull_request
+  - name: Test
+  - name: Deploy to Test PyPI release
+    if: branch = dev AND type != pull_request
+  - name: Create GitHub release
+    if: branch = master AND type != pull_request
+  - name: Deploy to PyPI release
+    if: tag IS present
diff --git a/README.md b/README.md
index 6348a17..a0438f0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# cloudshell-cli
-[![Build status](https://travis-ci.org/QualiSystems/cloudshell-cli.svg?branch=master)](https://travis-ci.org/QualiSystems/cloudshell-cli)
-[![codecov](https://codecov.io/gh/QualiSystems/cloudshell-cli/branch/master/graph/badge.svg)](https://codecov.io/gh/QualiSystems/cloudshell-cli)
+# CloudShell CLI
+[![Build status](https://travis-ci.org/QualiSystems/cloudshell-cli.svg?branch=dev)](https://travis-ci.org/QualiSystems/cloudshell-cli)
+[![codecov](https://codecov.io/gh/QualiSystems/cloudshell-cli/branch/dev/graph/badge.svg)](https://codecov.io/gh/QualiSystems/cloudshell-cli)
 [![PyPI version](https://badge.fury.io/py/cloudshell-cli.svg)](https://badge.fury.io/py/cloudshell-cli)
 [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black)
 
@@ -8,136 +8,260 @@
 <img src="https://github.com/QualiSystems/devguide_source/raw/master/logo.png"></img>
 </p>
 
-# CloudShell CLI
-A Python package providing an easy abstraction interface for CLI access and communication (Telnet, TCP, SSH etc.) for network devices.
+## Overview
+The cloudshell-cli open source Python package provides an easy-to-use interface for CLI access and communication (Telnet, TCP, SSH etc.) with network devices.
+
+**Note:** We use tox and pre-commit for testing. For details, see [Services description](https://github.com/QualiSystems/cloudshell-package-repo-template#description-of-services).
 
-# Download
-You can download a free version of CLI here. 
+## Key features
 
-# Installation
-CLI is available on PyPy, you can install it by invoking pip command _pip install cloudshell-cli_
+CloudShell CLI offers the following key features (For details, see (Usage)[#usage]): 
+* Multi-protocol communication, including **SSH** and **Telnet**.
+* **Session pool**: CloudShell CLI uses a session pool to store and manage sessions safely between multiple threads using the [Python queue module](https://docs.python.org/3.7/library/queue.html). The maximum session pool size and timeout period are customizable parameters:
+  * Maximum session pool size (`max_pool_size`) determines the maximum number of concurrent sessions in the session pool (default is 1).
+  * Timeout period (`pool_timeout`) determines the maximum time a thread can wait for a session (default is 100 seconds).
+* **cli service** allows CloudShell CLI to switch between the device's CLI modes.
+<br>*CloudShell CLI uses the `with` statement to reserve the session and move between the modes, as illustrated in the examples below.*
 
-## Contributing 
 
-We welcome community ideas and contributions. 
+## Installation
+```bash
+pip install cloudshell-cli
+```
+
+### Contributing 
 
-To provide feedback or request enhancements simply create an issue in the repository. 
-You can use the [Waffle Board](https://waffle.io/QualiSystems/cloudshell-cli) to add issues directly and catch up on the current backlog progress.
+We welcome community ideas and contributions, so if you have any feedback or would like to request enhancements, feel free to create an **Issue** in the repository. 
 
-### Contributing code
+#### Contributing code
 
 1. Fork the repository. 
 
 2. Make the code change. Make sure the tests pass. Add unit tests to cover any new behavior. We do require that the coverage does not decrease with the new code.
 
-3. Submit a PR 
+3. Submit a **Pull Request**.
+
+## Usage
+
+CloudShell CLI is highly modular and implements many programming interfaces. 
+
+### Session
+
+**Session** is a service that initializes the session by declaring the session parameters and handles communication with the device. Depending on the communication protocol, you will need to either use **SSHSession** or **TelnetSession**.
+
+_**Note:** Creating a session object does not create a connection to the device. This is done by `SessionPoolManager` as part of the CLI's `get_session` command. Once the connection is established, the new session is stored and managed by the `SessionPoolManager`._ 
 
-### Running the tests
+To initialize the session, we need to pass the  parameters:
+- **IP address**
+- **Username**
+- **Password**
 
-This repository uses nose to run tests. To run all tests run the following from command line:
+**Example: Initializing the session**
 
-```Bash
-nosetests
+```python
+from cloudshell.cli.session.ssh_session import SSHSession
+
+
+session = SSHSession(host='localhost', username='admin', password='Pass1234')
 ```
 
-# Examples
--------------------------------------------------------------------------------------------------------------------
+### CommandMode
+
+**CommandMode** enables you to define each mode on your device (in other words, how to enter and leave the mode, and the expected prompt). For example, most network devices include a root (or enable mode) and a configuration mode.
 
-Cloudshell CLI is highly modular and implements many programming interfaces. There is an usage of simple CLI commnads as well as an advanced usage for more complicated behaviours. 
+CommandMode uses the following basic parameters:
+* **prompt**: (Mandatory) The expected command-line prompt. CLI will 
+* **enter_command**: (Optional) The CLI command to execute in order to enter a certain mode. For example, to enter config mode in Cisco, you need to run something like: 
+```bash
+nexus#
+nexus# configure terminal
+nexus(config)#
+```
+* **exit_command**: (Optional) The command to execute in order to exit a certain mode.
+* **parent_mode**: (Optional) Parameter that defines the preceding CLI mode.
+
+_**Note:** You're welcome to check the CommandMode docstring for additional parameters._
+
+**Example - Declaring a single command mode:**
+```python
+from cloudshell.cli.command_mode import CommandMode
+
+
+mode = CommandMode(prompt='*#')
+
+```
+
+**Example - Declaring two command modes and the hierarchy using `parent_mode`):**
+
+```python
+from cloudshell.cli.command_mode import CommandMode
+
+
+enable_mode = CommandMode('*>')
+config_mode = CommandMode('*#', enter_command="configure terminal", exit_command="exit", parent_mode=enable_mode)
+
+```
+
+### CLI service
+**cli service** is the service that manages the sessions and command modes and allows us to send commands to the device and switch between the modes automatically. For example, if we have multiple command modes, cli service is able to move back and forth between these modes following the hierarchy defined by the `parent_mode` parameter of each commmand mode.
+
+**Example - Executing 'show interfaces'**
+
+Now that we've learned how to define the session and declare the command modes, we can start using them by creating a CLI object and passing the defined session and command modes.
 
-## Simple CLI usage:
 ```python
 from cloudshell.cli.cli import CLI
 from cloudshell.cli.session.ssh_session import SSHSession
 from cloudshell.cli.command_mode import CommandMode
 
-class CreateSessionSimpleCase():
 
-    def create_my_session(self):
+cli = CLI()
+mode = CommandMode(r'my_prompt_regex') # for example r'%\s*$'
+
+session_types = [SSHSession(host='ip_address',username='user_name',password='password')]
+
+# extract a session from the pool, send the command and return the session to the pool upon completing the "with"
+# block:
+with cli.get_session(session_types, mode) as cli_service:
+    out = cli_service.send_command('show interfaces')
+    print(out)
+
+```
+
+**Example - Providing different communication protocols**
+
+In the previous example, we assumed the device works over SSH. However, you can specify multiple communication protocols (for example, SSH and Telnet). In such a case, CloudShell will attempt connection with each provided protocol until it finds the one that works.
+
+```python
+from cloudshell.cli.cli import CLI
+from cloudshell.cli.session.ssh_session import SSHSession
+from cloudshell.cli.session.telnet_session import TelnetSession
+from cloudshell.cli.command_mode import CommandMode
 
-        cli = CLI()
-        mode = CommandMode(r'my_prompt_regex') # for example r'%\s*$'
 
-        session_types = [SSHSession(host='ip_address',username='user_name',password='password')]
+cli = CLI()
+mode = CommandMode(r'my_prompt_regex') # for example r'%\s*$'
 
+session_types = [SSHSession(host='ip_address',username='user_name',password='password')]
 
-        with cli.get_session(session_types, mode) as default_session:
-            out = default_session.send_command('my command')
-            print(out)
+with cli.get_session(session_types, mode) as cli_service:
+    out = cli_service.send_command('show interfaces')
+    print(out)
 
 ```
-###Description:
+**Example - Using multiple command modes**
 
-In the above code we create a simple SSH connection to our device. We first import the required packages (lines 51-53), where:
-- CLI is an API providing access for creating the new session into the device. 
-- SSHSession is an API providing functions to declare the session parametrs, as well as functions to connect and dis-connect from the session. Note that you can create you're own session class similar to SSHSession structure.
-- CommandMode is an API providing you the ability to define each mode on your device. For example on switches there may be several modes as configuration mode, admin mode. So using the CommandMode interface you able to define how to enter each mode, how to leave mode, what is the expected prompt.
+To illustrate this point, the following example will execute a `show interfaces` in config_mode and then `show version` in enable_mode.
+First, CloudShell CLI will get a session to the device from the session pool (which will create a new one if empty). The cli service will use the session to access the device and detect the current mode (let's assume it's **enable_mode**). Then, it will automatically switch to config_mode by executing the `enter_command` parameter, as specified in the **config_mode**'s definition.
 
-## Advanced CLI usage (level I):
 ```python
 from cloudshell.cli.cli import CLI
 from cloudshell.cli.session.ssh_session import SSHSession
+from cloudshell.cli.session.telnet_session import TelnetSession
 from cloudshell.cli.command_mode import CommandMode
 
-class CreateSessionAdvancedCase():
-    def create_my_session(self):
-        cli = CLI()
-        mode = CommandMode(r'my_prompt_regex')
-        session_types = [SSHSession(host='ip_address',username='user_name',password='password')]
-        with cli.get_session(session_types, mode) as default_session:
-            out = default_session.send_command('my command')
-            print(out)
-            config_command_mode = CommandMode(r'my_config_prompt_regex')
-            with default_session.enter_mode(config_command_mode) as config_session:
-                out = config_session.send_command('my config mode command')
-                print(out)
+
+hostname = "192.168.1.1"
+username = "admin"
+password = "admin"
+
+enable_mode = CommandMode('*>')
+config_mode = CommandMode('*#', enter_command="configure", exit_command="exit", parent_mode=enable_mode)
+
+sessions = [SSHSession(hostname, username, password), TelnetSession(hostname, username, password)]
+
+cli = CLI()
+
+#----------------------------------------------------------
+# extract a session from the pool, switch to config mode, send the command, and return the session to the pool
+# when "with" block finished:
+with cli.get_session(sessions, config_mode) as cli_service:
+    output = cli_service.send_command('show interfaces')
+
+# extract the session, switch to enable mode, send the command and return the session to the pool after the
+# "with" block:
+with cli.get_session(sessions, enable_mode) as cli_service:
+    output = cli_service.send_command('show version')
+#----------------------------------------------------------
+# OR switch between the modes by enter_mode command:
+#----------------------------------------------------------
+# extract the session, switch to enable mode, send the command, enter config mode (2nd "with" block),
+# send commands, upon completing the 2nd "with" block, return to the previous mode (enable mode) and 
+# return the session to the pool:
+with cli.get_session(sessions, enable_mode) as cli_service:
+    output = cli_service.send_command('show version')
+    with cli_service.enter_mode(config_mode) as config_cli_service:
+        print(config_cli_service.send_command('show interfaces'))
+        output = config_cli_service.send_command('show configuration')
+#----------------------------------------------------------
+
 ```
-## CLI Usage for Cloudshell users
-In this case we must to get all the data about our device and session from the resource configured on cloudshell. For this porpuse we will use the context object receiving from cloudshell. 
-For the sake of our example, we will define the context object here and just use it with our example.
+
+**Example - Switching back to the previous mode**
+
+In the previous example, we learned how to switch from enable_mode to config_mode and send commands in each. Now let's say you're in config_mode and want to return to enable_mode. To do so, simply return to the enable_mode block's indentation and specify your command. 
+
 ```python
-def create_context():
-    from cloudshell.shell.core.context import ResourceCommandContext, ResourceContextDetails, ReservationContextDetails
-    context = ResourceCommandContext()
-    context.resource = ResourceContextDetails()
-    context.resource.name = 'CiscoIOS'
-    context.reservation = ReservationContextDetails()
-    context.reservation.reservation_id = '5695cf87-a4f3-4447-a08a-1a99a936010e'
-    context.reservation.owner_user = 'admin'
-    context.reservation.owner_email = 'fake@qualisystems.com'
-    context.reservation.environment_path ='Environment-Exmaple'
-    context.reservation.environment_name = 'Environment-Exmaple'
-    context.reservation.domain = 'Global'
-    context.resource.attributes = {}
-    context.resource.attributes['CLI Connection Type'] = 'SSH'
-    context.resource.attributes['User'] = 'cisco_user'
-    context.resource.attributes['AdminUser'] = 'cisco_admin_user'
-    context.resource.attributes['Console Password'] = '3M3u7nkDzxWb0aJ/IZYeWw=='
-    context.resource.attributes['Password'] = 'PgkOScppedeEbHGHdzpnrw=='
-    context.resource.attributes['Enable Password'] = 'PgkOScppedeEbHGHdzpnrw=='
-    context.resource.address = '192.168.1.2'
-    context.resource.attributes['SNMP Version'] = '2'
-    context.resource.attributes['SNMP Read Community'] = 'Test1234'
-    context.resource.attributes['Model'] = 'Enterprises.2011.2.23.339'
-    context.resource.attributes['AdminPassword'] ='DxTbqlSgAVPmrDLlHvJrsA=='
-    context.resource.attributes['Vendor'] = 'Cisco'
-    return context
+from cloudshell.cli.cli import CLI
+from cloudshell.cli.session.ssh_session import SSHSession
+from cloudshell.cli.session.telnet_session import TelnetSession
+from cloudshell.cli.command_mode import CommandMode
+
+
+hostname = "192.168.1.1"
+username = "admin"
+password = "admin"
+
+enable_mode = CommandMode('*>')
+config_mode = CommandMode('*#', enter_command="configure", exit_command="exit", parent_mode=enable_mode)
+
+sessions = [SSHSession(hostname, username, password), TelnetSession(hostname, username, password)]
+
+cli = CLI()
+
+with cli.get_session(sessions, enable_mode) as cli_service:
+    output = cli_service.send_command('show version')
+    with cli_service.enter_mode(config_mode) as config_cli_service:
+        print(config_cli_service.send_command('show interfaces'))
+        output = config_cli_service.send_command('show configuration')
+    # switch back to enable_mode and send a command
+    cli_service.send_command("some command")
+    
 ```
 
-Next we will explain how to define modes for the session. For example in routers there may be a default mode, a configuration mode an admin mode etc.
-In the mode object we can define expected_map, which is used in cases when we expect to questions from the cli we specify the required response. We can also define error_map to catch run time errors on the switch.
+### Action and error maps
+
+In this chapter, we will learn how to set predefined actions to specific cli prompts and responses. For example, what to do in the event of a yes/no prompt or when receiving an "inavlid command" error. 
+
+* **Error map**: Dictionary where the key is the regex pattern of the expected prompt or cli response and the value is the exception message we want to raise. For example, `{r"Invalid command": "Failed to execute command"}`
+* **Action map**: Dictionary where the key is the regex pattern of the expected prompt or cli response and the value is the function to be performed. For Example, `{r"y/n": lambda session, logger: session.send_line("y", logger)}`
+
+**Example:**
 
-First we wiil define a default actions class 
 ```python
-class DefaultActions(object):
-    def __init__(self):
-        pass
-    def actions(self, session, logger):
-        out = session.hardware_expect('echo default' , DefaultCommandMode.PROMPT,
-                                      logger,action_map={r'%\s*$': lambda session, logger: session.send_line('cli', logger)})
-```
+from cloudshell.cli.cli import CLI
+from cloudshell.cli.session.ssh_session import SSHSession
+from cloudshell.cli.session.telnet_session import TelnetSession
+from cloudshell.cli.command_mode import CommandMode
+
+
+hostname = "192.168.1.1"
+username = "admin"
+password = "admin"
 
-With the DefaultActions class we define all the commands we need to initiate the console on the session. For example on a switch a default action can be "set cli screen-length 0" to print all in once to the screen.
-actions function is the functon that invokes TODO
+enable_mode = CommandMode('*>')
+
+sessions = [SSHSession(hostname, username, password), TelnetSession(hostname, username, password)]
+
+cli = CLI()
+
+my_action_map = {r"--More--": lambda session, logger: session.send_line("", logger),  
+                 # "session.send_line(command, logger)" sends command and "Enter" key line break
+                 r"\(yes/no/abort\)": lambda session, logger: session.send_line("abort", logger),
+                }
+my_error_map = {r"^[Ii]nvalid [Cc]ommand": "My error message",
+               }
+with cli.get_session(sessions, enable_mode) as cli_service:
+    output = cli_service.send_command('show version', action_map=my_action_map, error_map=my_error_map)
+```
 
-We use tox and pre-commit for testing. [Services description](https://github.com/QualiSystems/cloudshell-package-repo-template#description-of-services)
diff --git a/cloudshell/cli/cli.py b/cloudshell/cli/cli.py
new file mode 100644
index 0000000..fe8f031
--- /dev/null
+++ b/cloudshell/cli/cli.py
@@ -0,0 +1,13 @@
+"""Deprecated,CLI moved to cloudshell.cli.service.cli.CLI."""
+import warnings
+
+from cloudshell.cli.service.cli import CLI
+
+warnings.warn(
+    "CLI module moved from cloudshell.cli.cli to cloudshell.cli.service.cli. "
+    "Use import cloudshell.cli.service.cli.",
+    DeprecationWarning,
+    stacklevel=2,
+)
+warnings.simplefilter("default", DeprecationWarning)
+__all__ = [CLI]
diff --git a/tox.ini b/tox.ini
index 1dd29f2..5305182 100644
--- a/tox.ini
+++ b/tox.ini
@@ -29,7 +29,9 @@ commands = pre-commit run --all-files --show-diff-on-failure
 
 [testenv:build]
 skip_install = true
-commands = python setup.py sdist --format zip
+commands =
+    python setup.py sdist --format zip
+    python setup.py bdist_wheel --universal
 
 [isort]
 line_length = 88
diff --git a/version.txt b/version.txt
index cc868b6..4d54dad 100644
--- a/version.txt
+++ b/version.txt
@@ -1,1 +1,1 @@
-4.0.1
\ No newline at end of file
+4.0.2


Test patch:
diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py
new file mode 100644
index 0000000..a6bacd1
--- /dev/null
+++ b/tests/cli/test_cli.py
@@ -0,0 +1,19 @@
+import unittest
+import warnings
+
+
+class TestImportCliDeprecated(unittest.TestCase):
+    def test_deprecated_import(self):
+        with warnings.catch_warnings(record=True) as w:
+            # Cause all warnings to always be triggered.
+            warnings.simplefilter("always")
+            # Trigger a warning.
+            from cloudshell.cli.cli import CLI  # noqa: F401
+
+            # verify
+            self.assertEqual(1, len(w))
+            self.assertEqual(DeprecationWarning, w[-1].category)
+            self.assertIn(
+                "module moved from cloudshell.cli.cli to cloudshell.cli.service.cli",
+                str(w[-1].message),
+            )