From 5b3b7b83292341eebdac440daabe67715cba5395 Mon Sep 17 00:00:00 2001
From: sadanandhegde <sadanandhegde@users.noreply.github.com>
Date: Thu, 30 Sep 2021 15:23:22 -0400
Subject: [PATCH 01/47] Update Sandbox.py

Updated with latest code
---
 Sandbox-API/Sandbox.py | 532 ++++++++++++++++++++++++-----------------
 1 file changed, 312 insertions(+), 220 deletions(-)

diff --git a/Sandbox-API/Sandbox.py b/Sandbox-API/Sandbox.py
index 67ded25..ee72f02 100644
--- a/Sandbox-API/Sandbox.py
+++ b/Sandbox-API/Sandbox.py
@@ -1,265 +1,357 @@
-import requests
 import json
+import requests
 
 
-class Sandbox:
-    """
-    Sandbox Python API wrapper
+class SandboxAPI:
+    """ Python wrapper for CloudShell Sandbox API
     """
-
-    def __init__(self, config_file):
-        quali_config = json.load(open(config_file, 'r'))
-        self.server_address = 'http://{}:{}/api'.format(quali_config['server_name'], quali_config['server_port'])
-        self.username = quali_config['username']
-        self.password = quali_config['password']
-        self.domain = quali_config['domain']
-        self.auth_code = ''
-        self.headers = ''
-
-    def _request_and_parse(self, request_type, url_str, json_dict={}, data_dict={}):
+    def __init__(self, host, username, password, domain='Global', port=82):
+        """Initializes and logs in Sandbox
+        :param str host: hostname of IP address of sandbox API server
+        :param str username: CloudShell username
+        :param str password: CloudShell password
+        :param str domain: CloudShell domain (default=Global)
+        :param int port: Sandbox API port number(default=82)
         """
+        self._server_url = 'http://{}:{}/api'.format(host, port)
+        response = requests.put('{}/login'.format(self._server_url),
+                                json={'username': username, 'password': password, 'domain': domain})
 
-        :param request_type:
-        :param url_str:
-        :param json_dict:
-        :param data_dict:
-        :return:
-        """
-
-        response = ''
-        if request_type.lower() == 'put':
-            response = requests.put(url_str, json=json_dict, headers=self.headers)
-
-        elif request_type.lower() == 'get':
-            response = requests.get(url_str, json=json_dict, headers=self.headers)
-
-        elif request_type.lower() == 'post':
-            response = requests.post(url_str, json=json_dict, headers=self.headers, data=json.dumps(data_dict))
-
-        if not response.ok:
-            raise Exception('Error code: {}\nError text: {}\nP{} failed, exiting'.format(response.status_code,
-                                                                                  json.loads(response.text)[
-                                                                                      'message'], url_str))
-        return response
-
-    def login(self):
-        """Login and set some internal variables
-        """
-        url_str = self.server_address + '/login'
-        json_dict = {'username': self.username, 'password': self.password, 'domain': self.domain}
-        response = self._request_and_parse('put', url_str, json_dict)
-        self.auth_code = "Basic " + response.content[1:-1]
-        self.headers = {"Authorization": self.auth_code, "Content-Type": "application/json"}
+        self._headers = {"Authorization": "Basic " + response.content[1:-1].decode('utf-8'),
+                         "Content-Type": "application/json"}
 
     def get_blueprints(self):
-        """Get all blueprints details
-        :return: <dict> Dict of blueprints and their ids
-        """
-        url_str = '{}{}'.format(self.server_address, '/v1/blueprints')
-        response = self._request_and_parse('get', url_str)
-
-        # parse the output
-        parsed_response = json.loads(response.content)
-        blueprint_names = [blueprint['name'].encode('utf-8') for blueprint in parsed_response]
-        blueprint_ids = [blueprint['id'].encode('utf-8') for blueprint in parsed_response]
-        blueprint_dict = dict(zip(blueprint_names, blueprint_ids))
-
-        # return a dictionary of blueprints names and their ids
-        return blueprint_dict
-
-    def get_blueprint_id(self, blueprint_name):
-        """Return blueprint id, given blueprint name
-        :param blueprint_name: Name of the blueprint
-        :return: blueprint_id
+        """Get list of blueprints
+        :return:
         """
-
-        # Get all blueprints and see if blueprint_name exists in the list
-        blueprints = self.get_blueprints()
-        if blueprint_name not in blueprints.iterkeys():
-            raise Exception(
-                'Blueprint "{}" not found, exiting'.format(blueprint_name))
-
-        # If exists, return name of blueprint
-        return blueprints[blueprint_name]
+        response = requests.get('{}/v2/blueprints'.format(self._server_url), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
     def get_blueprint_details(self, blueprint_id):
-        """Returns a dict of the blueprint, given the blueprint id
-        :param blueprint_id: blueprint_id
-        :return: dict of name, estimated_setup_duration, description of the blueprint
-        """
-        url_str = '{}{}{}'.format(self.server_address, '/v1/blueprints/', blueprint_id)
-        response = self._request_and_parse('get', url_str)
-        parsed_blueprint_details = json.loads(response.content)
-        return_dict = {'name': parsed_blueprint_details['name'],
-                       'estimated_setup_duration': parsed_blueprint_details['estimated_setup_duration'],
-                       'description': parsed_blueprint_details['description']}
-        return return_dict
-
-    def get_blueprint_details_by_name(self, blueprint_name):
-        """Create a sandbox from the provided blueprint name
-        :param blueprint_name: blueprint name
-        :return: dict of name, estimated_setup_duration, description of the blueprint
+        """Get details of a specific blueprint
+        :param blueprint_id: Blueprint name or id
+        :return:
         """
-        blueprint_id = self.get_blueprint_id(blueprint_name)
-        return self.get_blueprint_details(blueprint_id)
+        response = requests.get('{}/v2/blueprints/{}'.format(self._server_url, blueprint_id), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
-    def start_sandbox(self, blueprint_id, duration, sandbox_name=''):
+    def start_sandbox(self, blueprint_id, duration, sandbox_name=None, parameters=None, permitted_users=None):
         """Create a sandbox from the provided blueprint id
-        :param blueprint_id: blueprint_id
-        :param duration: duration in minutes
-        :param sandbox_name: name of the sandbox, same as blueprint if name=''
-        :return: if success sandbox_id, else False
+        :param list permitted_users: list of permitted users ex: ['user1', 'user2']
+        :param list parameters: List of dicts, input parameters in the format ex: [{"name": "Version",
+            "value": "3.0"}, {"name": "Build Number", "value": "5"}]
+        :param str blueprint_id: blueprint_id or name
+        :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
+        :param str sandbox_name: name of the sandbox, same as blueprint if name=''
+        :return:
         """
-
-        # Do some parameter validation
-        try:
-            int(duration)
-        except ValueError:
-            raise Exception('Duration "{}" has to be integer'.format(duration))
-
-        duration = 'PT{}M'.format(duration)
-        if sandbox_name == '':
+        if not sandbox_name:
             sandbox_name = self.get_blueprint_details(blueprint_id)['name']
-
-        url_str = '{}{}{}/{}'.format(self.server_address, '/v1/blueprints/', blueprint_id, 'start')
         data_dict = {"duration": duration, "name": sandbox_name}
-        response = self._request_and_parse('post', url_str, data_dict=data_dict)
+        if permitted_users:
+            data_dict['permitted_users'] = permitted_users
+        if parameters:
+            data_dict["params"] = parameters
+
+        response = requests.post('{}/v2/blueprints/{}/start'.format(self._server_url, blueprint_id),
+                                 headers=self._headers,
+                                 data=json.dumps(data_dict))
         if response.ok:
-            return json.loads(response.content)['id']
-        else:
-            return response.ok
-
-    def start_sandbox_by_name(self, blueprint_name, duration, sandbox_name=''):
-        """Create a sandbox from the provided blueprint name
-        :param blueprint_name: blueprint_name
-        :param duration: duration in minutes
-        :param sandbox_name: sandbox name
-        :return: if success sandbox_id, else False
-        """
-        blueprint_id = self.get_blueprint_id(blueprint_name)
-        if sandbox_name == '':
-            sandbox_name = blueprint_name
-        return self.start_sandbox(blueprint_id, duration, sandbox_name)
+            return json.loads(response.content)
+        return response.reason
 
     def get_sandboxes(self):
-        """Returns a dictionary of all sandboxes name and their ids
-        :return: A dict of sandbox ids and names
+        """Get list of sandboxes
+        :return:
         """
-        url_str = '{}{}'.format(self.server_address, '/v1/sandboxes')
-        response = self._request_and_parse('get', url_str)
+        response = requests.get('{}/v2/sandboxes'.format(self._server_url), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
-        # parse the output
-        parsed_response = json.loads(response.content)
-        sandbox_names = [sandbox['name'].encode('utf-8') for sandbox in parsed_response]
-        sandbox_ids = [sandbox['id'].encode('utf-8') for sandbox in parsed_response]
-        sandbox_dict = dict(zip(sandbox_ids, sandbox_names))
+    def get_sandbox_details(self, sandbox_id):
+        """Get details of the given sandbox id
+        :param sandbox_id: Sandbox id
+        :return:
+        """
+        response = requests.get('{}/v2/sandboxes/{}'.format(self._server_url, sandbox_id), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
-        # return a dictionary of sandboxes names and their ids
-        return sandbox_dict
+    def get_sandbox_activity(self, sandbox_id):
+        """Get list of sandbox activity
+        :param str sandbox_id: Sandbox id
+        :return:
+        """
+        response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._server_url, sandbox_id, 'activity'),
+                                headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
-    def get_sandbox_details(self, sandbox_id):
-        """Returns a dictionary of the sandbox, its name, type and state
-        :param sandbox_id: <str> Sandbox id
-        :return: dictionary of sandbox name, type and state
+    def get_sandbox_commands(self, sandbox_id):
+        """Get list of sandbox commands
+        :param str sandbox_id: Sandbox id
+        :return:
         """
+        response = requests.get('{}/v2/sandboxes/{}/commands'.format(self._server_url, sandbox_id),
+                                headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
-        # Get info from cloudshell
-        url_str = '{}{}{}'.format(self.server_address, '/v1/sandboxes/', sandbox_id)
-        response = self._request_and_parse('get', url_str)
+    def get_sandbox_command_details(self, sandbox_id, command_name):
+        """Get details of specific sandbox command
+        :param str sandbox_id: Sandbox id
+        :param str command_name: Sandbox command to be executed
+        :return:
+        """
+        response = requests.get('{}/v2/sandboxes/{}/commands/{}'.format(
+            self._server_url, sandbox_id, command_name),
+            headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def sandbox_command_start(self, sandbox_id, command_name, params=None):
+        """Start a sandbox command
+        :param str sandbox_id: Sandbox id
+        :param str command_name: Sandbox command to be executed
+        :param dict params: parameters to be passed to the command ex: {"params": [{"name": "WaitTime", "value": "1"}],
+            "printOutput": True}
+        :return:
+        """
+        if params:
+            response = requests.post('{}/v2/sandboxes/{}/commands/{}/start'.format(
+                self._server_url, sandbox_id, command_name),
+                data=json.dumps(params),
+                headers=self._headers)
+        else:
+            response = requests.post('{}/v2/sandboxes/{}/commands/{}/start'.format(
+                self._server_url, sandbox_id, command_name),
+                headers=self._headers)
 
-        # parse the information
-        parsed_blueprint_details = json.loads(response.content)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
-        # prepare a dictionary to return
-        return_dict = {'name': parsed_blueprint_details['name'],
-                       'type': parsed_blueprint_details['type'],
-                       'state': parsed_blueprint_details['state']}
-        return return_dict
+    def get_sandbox_components(self, sandbox_id):
+        """Get list of sandbox components
+        :param str sandbox_id: Sandbox id
+        :return: Returns a list of tuples of component type, name and id
+        """
+        response = requests.get('{}/v2/sandboxes/{}/components'.format(
+            self._server_url, sandbox_id), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def get_sandbox_component_details(self, sandbox_id, component_id):
+        """Get details of components in sandbox
+        :param str sandbox_id: Sandbox id
+        :param str component_id: Component id
+        :return: Returns a tuple of component type, name, id, address, description
+        """
+        response = requests.get('{}/v2/sandboxes/{}/components/{}'.format(self._server_url, sandbox_id, component_id),
+                                headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
-    def get_sandboxes_details_by_name(self, sandbox_name):
+    def get_sandbox_component_commands(self, sandbox_id, component_id):
+        """Get list of commands for a particular component in sandbox
+        :param str sandbox_id: Sandbox id
+        :param str component_id: Component id
+        :return:
         """
-        :param sandbox_name: Sandbox name
-        :return: dictionary of sandbox name, type and state
+        response = requests.get('{}/v2/sandboxes/{}/components/{}/commands'.format(
+            self._server_url, sandbox_id, component_id), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def get_sandbox_component_command_details(self, sandbox_id, component_id, command):
+        """Get details of a command of sandbox component
+        :param str sandbox_id: Sandbox id
+        :param str component_id: Component id
+        :param str command: Command name
+        :return:
         """
-        return_dict = {}
-        sandbox_ids = self.get_sandbox_ids(sandbox_name)
-        for sandbox_id in sandbox_ids:
-            return_dict[sandbox_id] = self.get_sandbox_details(sandbox_id)
-        return return_dict
-
-    def get_sandbox_ids(self, sandbox_name):
-        """Returns the sandbox ids for the given sandbox name
-        :param sandbox_name: Sandbox name
-        :return: Sandbox id
+        response = requests.get('{}/v2/sandboxes/{}/components/{}/commands/{}'.format(
+            self._server_url, sandbox_id, component_id, command), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def sandbox_component_command_start(self, sandbox_id, component_id, command_name, params=None):
+        """Start a command on sandbox component
+        :param str sandbox_id: Sandbox id
+        :param str component_id: Component id
+        :param str command_name: Command name
+        :param dict params: parameters to be passed to the command ex:
+            {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
+        :return:
         """
-        sandboxes = self.get_sandboxes()
-        if sandbox_name not in sandboxes.itervalues():
-            raise Exception(
-                'Sandbox "{}" not found, exiting'.format(sandbox_name))
+        if params:
+            response = requests.post('{}/v2/sandboxes/{}/components/{}/commands/{}/start'.format(
+                self._server_url, sandbox_id, component_id, command_name), data=json.dumps(params),
+                headers=self._headers)
+        else:
+            response = requests.post('{}/v2/sandboxes/{}/components/{}/commands/{}/start'.format(
+                self._server_url, sandbox_id, component_id, command_name), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
-        sandbox_ids = [k for k, v in sandboxes.iteritems() if v == sandbox_name]
-        return sandbox_ids
+    def extend_sandbox(self, sandbox_id, duration):
+        """Extend the sandbox
+        :param str sandbox_id: Sandbox id
+        :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
+        :return:
+        """
+        data_dict = json.loads('{"extended_time": "' + duration + '"}')
+        response = requests.post('{}/v2/sandboxes/{}/extend'.format(self._server_url, sandbox_id),
+                                 data=json.dumps(data_dict), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def get_sandbox_output(self, sandbox_id):
+        """Get list of sandbox output
+        :param str sandbox_id: Sandbox id
+        :return:
+        """
+        response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._server_url, sandbox_id, 'output'),
+                                headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
 
     def stop_sandbox(self, sandbox_id):
         """Stop the sandbox given sandbox id
         :param sandbox_id: Sandbox id
-        :return: True if success, False if not
-        """
-
-        url_str = '{}{}{}/{}'.format(self.server_address, '/v1/sandboxes/', sandbox_id, 'stop')
-        response = self._request_and_parse('post', url_str)
-        return response.ok
-
-    def stop_sandboxes_by_name(self, sandbox_name):
-        """Stop all the sandboxes with the given sandbox name
-        :param sandbox_name: Sandbox name
-        :return: True if success, False if not
+        :return:
         """
-        sandbox_ids = self.get_sandbox_ids(sandbox_name)
-        for sandbox_id in sandbox_ids:
-            self.stop_sandbox(sandbox_id)
+        response = requests.post('{}/v2/sandboxes/{}/stop'.format(self._server_url, sandbox_id), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+
+usage = """
+    bp_name = 'Environment1'
+    bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa'
+    sb_duration = 'PT10M'
+
+    sb_cmd = 'blueprint_power_cycle'
+    sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True}
+    sb_component = 'Demo Resource 1'
+    sb_component_cmd = 'Hello'
+    sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
+
+    sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global')
+    r = sandbox_api.get_blueprints()
+    print(r)
+    r = sandbox_api.get_blueprint_details(bp_name)
+    print(r)
+    r = sandbox_api.get_blueprint_details(bp_id)
+    print(r)
+    r = sandbox_api.start_sandbox(bp_name, sb_duration)
+    print(r)
+    sb_id = r['id']
+    r = sandbox_api.get_sandboxes()
+    print(r)
+    r = sandbox_api.get_sandbox_details(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_commands(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup')
+    print(r)
+    r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params)
+    print(r)
+    r = sandbox_api.get_sandbox_components(sb_id)
+    print(r)
+    comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0]
+
+    r = sandbox_api.get_sandbox_component_details(sb_id, comp_id)
+    print(r)
+    r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id)
+    print(r)
+    r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd)
+    print(r)
+    r = sandbox_api.sandbox_component_command_start(
+        sb_id, comp_id, sb_component_cmd,
+        params=sb_comp_cmd_params)
+    print(r)
+    r = sandbox_api.extend_sandbox(sb_id, sb_duration)
+    print(r)
+    r = sandbox_api.get_sandbox_activity(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_output(sb_id)
+    print(r)
+    print(sandbox_api.stop_sandbox(sb_id))
+"""
 
 
 def main():
-    usage = """Usage:
-    # Init vars
-    blueprint_name = 'Sandbox Python API Test'
-    sandbox_name = 'Sandbox Python API Test'
-    config_file = 'quali_config.json'
-
-    my_sandbox = Sandbox(config_file=config_file)
-
-    my_sandbox.login()
-
-    print my_sandbox.get_blueprints()
-    blueprint_id = my_sandbox.get_blueprint_id(blueprint_name=blueprint_name)
-    print "Blueprint Id:", blueprint_id
-    print my_sandbox.get_blueprint_details(blueprint_id=blueprint_id)
-    print my_sandbox.get_blueprint_details_by_name(blueprint_name=blueprint_name)
-
-    print my_sandbox.start_sandbox(blueprint_id=blueprint_id, duration='20', sandbox_name='')
-    print my_sandbox.start_sandbox_by_name(blueprint_name=blueprint_name, duration='20', sandbox_name='')
-    print my_sandbox.get_sandboxes()
-    sandbox_id = my_sandbox.get_sandbox_ids(sandbox_name=sandbox_name)
-    print sandbox_id
-    print my_sandbox.get_sandbox_details(sandbox_id=sandbox_id[0])
-    print my_sandbox.get_sandboxes_details_by_name(sandbox_name=sandbox_name)
-    print my_sandbox.stop_sandbox(sandbox_id=sandbox_id[0])
-    print my_sandbox.stop_sandboxes_by_name(sandbox_name=sandbox_name)
-    """
-
-    print usage
+    bp_name = 'Environment1'
+    bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa'
+    sb_duration = 'PT10M'
+
+    sb_cmd = 'blueprint_power_cycle'
+    sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True}
+    sb_component = 'Demo Resource 1'
+    sb_component_cmd = 'Hello'
+    sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
+
+    sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global')
+    r = sandbox_api.get_blueprints()
+    print(r)
+    r = sandbox_api.get_blueprint_details(bp_name)
+    print(r)
+    r = sandbox_api.get_blueprint_details(bp_id)
+    print(r)
+    r = sandbox_api.start_sandbox(bp_name, sb_duration)
+    print(r)
+    sb_id = r['id']
+    r = sandbox_api.get_sandboxes()
+    print(r)
+    r = sandbox_api.get_sandbox_details(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_commands(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup')
+    print(r)
+    r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params)
+    print(r)
+    r = sandbox_api.get_sandbox_components(sb_id)
+    print(r)
+    comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0]
+
+    r = sandbox_api.get_sandbox_component_details(sb_id, comp_id)
+    print(r)
+    r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id)
+    print(r)
+    r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd)
+    print(r)
+    r = sandbox_api.sandbox_component_command_start(
+        sb_id, comp_id, sb_component_cmd,
+        params=sb_comp_cmd_params)
+    print(r)
+    r = sandbox_api.extend_sandbox(sb_id, sb_duration)
+    print(r)
+    r = sandbox_api.get_sandbox_activity(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_output(sb_id)
+    print(r)
+    print(sandbox_api.stop_sandbox(sb_id))
 
 
 if __name__ == '__main__':
+    print(usage)
     main()
-
-
-# # Contents of quali_config.json
-# {
-#   "server_name": "localhost",
-#   "server_port": "82",
-#   "username": "USERNAME",
-#   "password": "PASSWORD",
-#   "domain": "DOMAIN"
-# }

From 75fcf69dc730459368bc22a3245970113de0e2a0 Mon Sep 17 00:00:00 2001
From: sadanandhegde <sadanandhegde@users.noreply.github.com>
Date: Thu, 30 Sep 2021 15:24:19 -0400
Subject: [PATCH 02/47] Add files via upload

Initial commit
---
 Sandbox-API/setup.py | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)
 create mode 100644 Sandbox-API/setup.py

diff --git a/Sandbox-API/setup.py b/Sandbox-API/setup.py
new file mode 100644
index 0000000..3740fdc
--- /dev/null
+++ b/Sandbox-API/setup.py
@@ -0,0 +1,27 @@
+from setuptools import setup
+
+
+def readme():
+    with open('README.rst') as f:
+        return f.read()
+
+
+setup(
+    name='cloudshell_sandboxapi_wrapper',
+    version='1.0.0',
+    packages=['cloudshell_sandboxapi_wrapper'],
+    url='http://www.quali.com',
+    license='Apache 2.0',
+    author='sadanand.s',
+    author_email='sadanand.s@quali.com',
+    description='Python wrapper for CloudShell Sandbox API',
+    long_description=readme(),
+    classifiers=[
+        'License :: OSI Approved :: Apache Software License',
+        'Natural Language :: English',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3.8',
+    ],
+    install_requires=['requests']
+)

From 5be92b91ab133a6c80eae2587dd157f89e2181c7 Mon Sep 17 00:00:00 2001
From: sadanandhegde <sadanandhegde@users.noreply.github.com>
Date: Thu, 30 Sep 2021 15:25:09 -0400
Subject: [PATCH 03/47] Rename Sandbox-API/setup.py to setup.py

Moved to root dir
---
 Sandbox-API/setup.py => setup.py | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename Sandbox-API/setup.py => setup.py (100%)

diff --git a/Sandbox-API/setup.py b/setup.py
similarity index 100%
rename from Sandbox-API/setup.py
rename to setup.py

From 717c7eb4d0de37ca0f904e1010442258a59ee282 Mon Sep 17 00:00:00 2001
From: sadanandhegde <sadanandhegde@users.noreply.github.com>
Date: Thu, 30 Sep 2021 15:26:29 -0400
Subject: [PATCH 04/47] Update README.md

Update newer README
---
 README.md | 48 +++++++++++++++++++++++-------------------------
 1 file changed, 23 insertions(+), 25 deletions(-)

diff --git a/README.md b/README.md
index 046c114..306cc41 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,27 @@
-# Sandbox-API-Python
-Sandbox API Python wrapper
+CloudShell Sandbox API Wrapper
+==============================
 
-**Usage:**
-```python
-from Sandbox import Sandbox
-blueprint_name = "Sandbox Python API Test"
-sandbox_name = "Sandbox Python API Test"
-config_file = "quali_config.json"
+Installation
+*************
+::
 
-my_sandbox = Sandbox(config_file=config_file)
-my_sandbox.login()
+    pip install cloudshell_sandboxapi_wrapper
 
-my_sandbox.get_blueprints()
-blueprint_id = my_sandbox.get_blueprint_id(blueprint_name=blueprint_name)
-print "Blueprint Id:", blueprint_id
-my_sandbox.get_blueprint_details(blueprint_id=blueprint_id)
-my_sandbox.get_blueprint_details_by_name(blueprint_name=blueprint_name)
+Example Usage
+**************
+::
 
-my_sandbox.start_sandbox(blueprint_id=blueprint_id, duration='20', sandbox_name='')
-my_sandbox.start_sandbox_by_name(blueprint_name=blueprint_name, duration='20', sandbox_name='')
-my_sandbox.get_sandboxes()
-sandbox_id = my_sandbox.get_sandbox_ids(sandbox_name=sandbox_name)
-print "Sandbox Id:", sandbox_id
-my_sandbox.get_sandbox_details(sandbox_id=sandbox_id[0])
-my_sandbox.get_sandboxes_details_by_name(sandbox_name=sandbox_name)
-my_sandbox.stop_sandbox(sandbox_id=sandbox_id[0])
-my_sandbox.stop_sandboxes_by_name(sandbox_name=sandbox_name)
-```
+    from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI
+    sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT)
+    blueprints = sandbox.get_blueprints()
+    blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id']
+    sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME)
+    sandbox.stop_sandbox(sandbox_id)
+
+|
+
+:Note:
+  Tested on cloudshell 9.3 with Python 2.7/3.7/3.8.
+  For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API <https://help.quali.com/Online%20Help/9.3/Api-Guide/Content/API/CS-Snbx-API-Topic.htm>`_
+
+|

From 83aebc31c5dbc09cdba2fa6dabe4156a4516f5cc Mon Sep 17 00:00:00 2001
From: sadanandhegde <sadanandhegde@users.noreply.github.com>
Date: Thu, 30 Sep 2021 15:46:02 -0400
Subject: [PATCH 05/47] Rename README.md to README.rst

Change to README.rst
---
 README.md => README.rst | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename README.md => README.rst (100%)

diff --git a/README.md b/README.rst
similarity index 100%
rename from README.md
rename to README.rst

From 81a3b75501eeb95697e3854141c1c23db4611f1b Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Fri, 1 Oct 2021 10:48:40 +0300
Subject: [PATCH 06/47] added gitignore

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 .gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..62c8935
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.idea/
\ No newline at end of file

From 59eef4b2178518a9d31d6946c0edc0f237ba6797 Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Fri, 1 Oct 2021 15:45:00 +0300
Subject: [PATCH 07/47] refactored all post requests

---
 Sandbox-API/Sandbox.py                   | 357 ---------------------
 Sandbox-API/quali_config.json            |   7 -
 requirements.txt                         |   1 +
 {Sandbox-API => sandbox_api}/__init__.py |   0
 sandbox_api/sandbox_rest.py              | 381 +++++++++++++++++++++++
 setup.py                                 |   1 -
 6 files changed, 382 insertions(+), 365 deletions(-)
 delete mode 100644 Sandbox-API/Sandbox.py
 delete mode 100644 Sandbox-API/quali_config.json
 create mode 100644 requirements.txt
 rename {Sandbox-API => sandbox_api}/__init__.py (100%)
 create mode 100644 sandbox_api/sandbox_rest.py

diff --git a/Sandbox-API/Sandbox.py b/Sandbox-API/Sandbox.py
deleted file mode 100644
index ee72f02..0000000
--- a/Sandbox-API/Sandbox.py
+++ /dev/null
@@ -1,357 +0,0 @@
-import json
-import requests
-
-
-class SandboxAPI:
-    """ Python wrapper for CloudShell Sandbox API
-    """
-    def __init__(self, host, username, password, domain='Global', port=82):
-        """Initializes and logs in Sandbox
-        :param str host: hostname of IP address of sandbox API server
-        :param str username: CloudShell username
-        :param str password: CloudShell password
-        :param str domain: CloudShell domain (default=Global)
-        :param int port: Sandbox API port number(default=82)
-        """
-        self._server_url = 'http://{}:{}/api'.format(host, port)
-        response = requests.put('{}/login'.format(self._server_url),
-                                json={'username': username, 'password': password, 'domain': domain})
-
-        self._headers = {"Authorization": "Basic " + response.content[1:-1].decode('utf-8'),
-                         "Content-Type": "application/json"}
-
-    def get_blueprints(self):
-        """Get list of blueprints
-        :return:
-        """
-        response = requests.get('{}/v2/blueprints'.format(self._server_url), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_blueprint_details(self, blueprint_id):
-        """Get details of a specific blueprint
-        :param blueprint_id: Blueprint name or id
-        :return:
-        """
-        response = requests.get('{}/v2/blueprints/{}'.format(self._server_url, blueprint_id), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def start_sandbox(self, blueprint_id, duration, sandbox_name=None, parameters=None, permitted_users=None):
-        """Create a sandbox from the provided blueprint id
-        :param list permitted_users: list of permitted users ex: ['user1', 'user2']
-        :param list parameters: List of dicts, input parameters in the format ex: [{"name": "Version",
-            "value": "3.0"}, {"name": "Build Number", "value": "5"}]
-        :param str blueprint_id: blueprint_id or name
-        :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
-        :param str sandbox_name: name of the sandbox, same as blueprint if name=''
-        :return:
-        """
-        if not sandbox_name:
-            sandbox_name = self.get_blueprint_details(blueprint_id)['name']
-        data_dict = {"duration": duration, "name": sandbox_name}
-        if permitted_users:
-            data_dict['permitted_users'] = permitted_users
-        if parameters:
-            data_dict["params"] = parameters
-
-        response = requests.post('{}/v2/blueprints/{}/start'.format(self._server_url, blueprint_id),
-                                 headers=self._headers,
-                                 data=json.dumps(data_dict))
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandboxes(self):
-        """Get list of sandboxes
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes'.format(self._server_url), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_details(self, sandbox_id):
-        """Get details of the given sandbox id
-        :param sandbox_id: Sandbox id
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}'.format(self._server_url, sandbox_id), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_activity(self, sandbox_id):
-        """Get list of sandbox activity
-        :param str sandbox_id: Sandbox id
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._server_url, sandbox_id, 'activity'),
-                                headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_commands(self, sandbox_id):
-        """Get list of sandbox commands
-        :param str sandbox_id: Sandbox id
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/commands'.format(self._server_url, sandbox_id),
-                                headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_command_details(self, sandbox_id, command_name):
-        """Get details of specific sandbox command
-        :param str sandbox_id: Sandbox id
-        :param str command_name: Sandbox command to be executed
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/commands/{}'.format(
-            self._server_url, sandbox_id, command_name),
-            headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def sandbox_command_start(self, sandbox_id, command_name, params=None):
-        """Start a sandbox command
-        :param str sandbox_id: Sandbox id
-        :param str command_name: Sandbox command to be executed
-        :param dict params: parameters to be passed to the command ex: {"params": [{"name": "WaitTime", "value": "1"}],
-            "printOutput": True}
-        :return:
-        """
-        if params:
-            response = requests.post('{}/v2/sandboxes/{}/commands/{}/start'.format(
-                self._server_url, sandbox_id, command_name),
-                data=json.dumps(params),
-                headers=self._headers)
-        else:
-            response = requests.post('{}/v2/sandboxes/{}/commands/{}/start'.format(
-                self._server_url, sandbox_id, command_name),
-                headers=self._headers)
-
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_components(self, sandbox_id):
-        """Get list of sandbox components
-        :param str sandbox_id: Sandbox id
-        :return: Returns a list of tuples of component type, name and id
-        """
-        response = requests.get('{}/v2/sandboxes/{}/components'.format(
-            self._server_url, sandbox_id), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_component_details(self, sandbox_id, component_id):
-        """Get details of components in sandbox
-        :param str sandbox_id: Sandbox id
-        :param str component_id: Component id
-        :return: Returns a tuple of component type, name, id, address, description
-        """
-        response = requests.get('{}/v2/sandboxes/{}/components/{}'.format(self._server_url, sandbox_id, component_id),
-                                headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_component_commands(self, sandbox_id, component_id):
-        """Get list of commands for a particular component in sandbox
-        :param str sandbox_id: Sandbox id
-        :param str component_id: Component id
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/components/{}/commands'.format(
-            self._server_url, sandbox_id, component_id), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_component_command_details(self, sandbox_id, component_id, command):
-        """Get details of a command of sandbox component
-        :param str sandbox_id: Sandbox id
-        :param str component_id: Component id
-        :param str command: Command name
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/components/{}/commands/{}'.format(
-            self._server_url, sandbox_id, component_id, command), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def sandbox_component_command_start(self, sandbox_id, component_id, command_name, params=None):
-        """Start a command on sandbox component
-        :param str sandbox_id: Sandbox id
-        :param str component_id: Component id
-        :param str command_name: Command name
-        :param dict params: parameters to be passed to the command ex:
-            {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
-        :return:
-        """
-        if params:
-            response = requests.post('{}/v2/sandboxes/{}/components/{}/commands/{}/start'.format(
-                self._server_url, sandbox_id, component_id, command_name), data=json.dumps(params),
-                headers=self._headers)
-        else:
-            response = requests.post('{}/v2/sandboxes/{}/components/{}/commands/{}/start'.format(
-                self._server_url, sandbox_id, component_id, command_name), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def extend_sandbox(self, sandbox_id, duration):
-        """Extend the sandbox
-        :param str sandbox_id: Sandbox id
-        :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
-        :return:
-        """
-        data_dict = json.loads('{"extended_time": "' + duration + '"}')
-        response = requests.post('{}/v2/sandboxes/{}/extend'.format(self._server_url, sandbox_id),
-                                 data=json.dumps(data_dict), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_output(self, sandbox_id):
-        """Get list of sandbox output
-        :param str sandbox_id: Sandbox id
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._server_url, sandbox_id, 'output'),
-                                headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def stop_sandbox(self, sandbox_id):
-        """Stop the sandbox given sandbox id
-        :param sandbox_id: Sandbox id
-        :return:
-        """
-        response = requests.post('{}/v2/sandboxes/{}/stop'.format(self._server_url, sandbox_id), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-
-usage = """
-    bp_name = 'Environment1'
-    bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa'
-    sb_duration = 'PT10M'
-
-    sb_cmd = 'blueprint_power_cycle'
-    sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True}
-    sb_component = 'Demo Resource 1'
-    sb_component_cmd = 'Hello'
-    sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
-
-    sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global')
-    r = sandbox_api.get_blueprints()
-    print(r)
-    r = sandbox_api.get_blueprint_details(bp_name)
-    print(r)
-    r = sandbox_api.get_blueprint_details(bp_id)
-    print(r)
-    r = sandbox_api.start_sandbox(bp_name, sb_duration)
-    print(r)
-    sb_id = r['id']
-    r = sandbox_api.get_sandboxes()
-    print(r)
-    r = sandbox_api.get_sandbox_details(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_commands(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup')
-    print(r)
-    r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params)
-    print(r)
-    r = sandbox_api.get_sandbox_components(sb_id)
-    print(r)
-    comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0]
-
-    r = sandbox_api.get_sandbox_component_details(sb_id, comp_id)
-    print(r)
-    r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id)
-    print(r)
-    r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd)
-    print(r)
-    r = sandbox_api.sandbox_component_command_start(
-        sb_id, comp_id, sb_component_cmd,
-        params=sb_comp_cmd_params)
-    print(r)
-    r = sandbox_api.extend_sandbox(sb_id, sb_duration)
-    print(r)
-    r = sandbox_api.get_sandbox_activity(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_output(sb_id)
-    print(r)
-    print(sandbox_api.stop_sandbox(sb_id))
-"""
-
-
-def main():
-    bp_name = 'Environment1'
-    bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa'
-    sb_duration = 'PT10M'
-
-    sb_cmd = 'blueprint_power_cycle'
-    sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True}
-    sb_component = 'Demo Resource 1'
-    sb_component_cmd = 'Hello'
-    sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
-
-    sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global')
-    r = sandbox_api.get_blueprints()
-    print(r)
-    r = sandbox_api.get_blueprint_details(bp_name)
-    print(r)
-    r = sandbox_api.get_blueprint_details(bp_id)
-    print(r)
-    r = sandbox_api.start_sandbox(bp_name, sb_duration)
-    print(r)
-    sb_id = r['id']
-    r = sandbox_api.get_sandboxes()
-    print(r)
-    r = sandbox_api.get_sandbox_details(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_commands(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup')
-    print(r)
-    r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params)
-    print(r)
-    r = sandbox_api.get_sandbox_components(sb_id)
-    print(r)
-    comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0]
-
-    r = sandbox_api.get_sandbox_component_details(sb_id, comp_id)
-    print(r)
-    r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id)
-    print(r)
-    r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd)
-    print(r)
-    r = sandbox_api.sandbox_component_command_start(
-        sb_id, comp_id, sb_component_cmd,
-        params=sb_comp_cmd_params)
-    print(r)
-    r = sandbox_api.extend_sandbox(sb_id, sb_duration)
-    print(r)
-    r = sandbox_api.get_sandbox_activity(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_output(sb_id)
-    print(r)
-    print(sandbox_api.stop_sandbox(sb_id))
-
-
-if __name__ == '__main__':
-    print(usage)
-    main()
diff --git a/Sandbox-API/quali_config.json b/Sandbox-API/quali_config.json
deleted file mode 100644
index e1e47f4..0000000
--- a/Sandbox-API/quali_config.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "server_name": "SERVER_NAME_OR_IP",
-  "server_port": "82",
-  "username": "USERNAME",
-  "password": "PASSWORD",
-  "domain": "DOMAIN"
-}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..663bd1f
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+requests
\ No newline at end of file
diff --git a/Sandbox-API/__init__.py b/sandbox_api/__init__.py
similarity index 100%
rename from Sandbox-API/__init__.py
rename to sandbox_api/__init__.py
diff --git a/sandbox_api/sandbox_rest.py b/sandbox_api/sandbox_rest.py
new file mode 100644
index 0000000..f100dd8
--- /dev/null
+++ b/sandbox_api/sandbox_rest.py
@@ -0,0 +1,381 @@
+import json
+from typing import List
+
+import requests
+from dataclasses import dataclass, asdict
+
+
+class SandboxRestException(Exception):
+    pass
+
+
+@dataclass
+class InputParam:
+    name: str
+    value: str
+
+
+class SandboxRestApiClient:
+    """ Python wrapper for CloudShell Sandbox API """
+
+    def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False,
+                 api_version="v2"):
+        """ logs into api and sets headers for future requests """
+        protocol = "https" if is_https else "http"
+        self._base_url = f"{protocol}://{host}:{port}/api"
+        self._versioned_url = f"{self._base_url}/{api_version}"
+        self._session = requests.Session()
+        self._auth_headers = self._get_auth_headers(username, password, domain)
+
+    def _get_auth_headers(self, user_name: str, password: str, domain: str):
+        """
+        Get token from login response, then place token into auth headers on class
+        """
+        login_res = requests.put(url=f"{self._base_url}/login",
+                                 data=json.dumps({"username": user_name, "password": password, "domain": domain}),
+                                 headers={"Content-Type": "application/json"})
+
+        if not login_res.ok:
+            raise SandboxRestException(self._format_err(login_res, "Failed Login"))
+
+        login_token = login_res.text[1:-1]
+
+        auth_headers = {
+            'Authorization': f'Basic {login_token}',
+            'Content-Type': 'application/json'
+        }
+        return auth_headers
+
+    @staticmethod
+    def _format_err(response: requests.Response, custom_err_msg="Failed Api Call"):
+        err_msg = f"Response Code: {response.status_code}, Reason: {response.reason}"
+        if custom_err_msg:
+            err_msg = f"{custom_err_msg}. {err_msg}"
+        return err_msg
+
+    # SANDBOX POST REQUESTS
+    def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
+                      bp_params: List[InputParam] = None, permitted_users: List[str] = None):
+        """
+        Create a sandbox from the provided blueprint id
+        Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
+        """
+        # sandbox name will default to blueprint name if not passed
+        if not sandbox_name:
+            sandbox_name = self.get_blueprint_details(blueprint_id)['name']
+
+        data_dict = {"duration": duration, "name": sandbox_name}
+        if permitted_users:
+            data_dict['permitted_users'] = permitted_users
+        if bp_params:
+            data_dict["params"] = [asdict(x) for x in bp_params]
+
+        response = requests.post(f'{self._versioned_url}/blueprints/{blueprint_id}/start',
+                                 headers=self._auth_headers,
+                                 data=json.dumps(data_dict))
+        if not response.ok:
+            err_msg = self._format_err(response, f"Failed to start sandbox from blueprint{blueprint_id}")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None,
+                                 permitted_users: List[str] = None):
+        """ Create a persistent sandbox from the provided blueprint id """
+
+        # sandbox name will default to blueprint name if not passed
+        if not sandbox_name:
+            sandbox_name = self.get_blueprint_details(blueprint_id)['name']
+
+        data_dict = {"name": sandbox_name}
+        if permitted_users:
+            data_dict['permitted_users'] = permitted_users
+        if bp_params:
+            data_dict["params"] = [asdict(x) for x in bp_params]
+
+        response = requests.post(f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent',
+                                 headers=self._auth_headers,
+                                 data=json.dumps(data_dict))
+        if not response.ok:
+            err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint{blueprint_id}")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def start_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None,
+                              print_output=True):
+        """ Run a sandbox level command """
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start'
+        data_dict = {"printOutput": print_output}
+        params = [asdict(x) for x in params] if params else []
+        data_dict["params"] = params
+        response = requests.post(url, data=json.dumps(data_dict),headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"failed to start sandbox command {command_name}"))
+        return response.json()
+
+    def start_component_command(self, sandbox_id: str, component_id: str, command_name: str,
+                                params: List[InputParam] = None, print_output: bool = True):
+        """ Start a command on sandbox component """
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start'
+        data_dict = {"printOutput": print_output}
+        params = [asdict(x) for x in params] if params else []
+        data_dict["params"] = params
+        response = requests.post(url, data=json.dumps(data_dict),headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"failed to start component command {command_name}"))
+        return response.json()
+
+    def extend_sandbox(self, sandbox_id: str, duration: str):
+        """Extend the sandbox
+        :param str sandbox_id: Sandbox id
+        :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
+        :return:
+        """
+        data_dict = {"extended_time": duration}
+        response = requests.post(f'{self._base_url}/sandboxes/{sandbox_id}/extend',
+                                 data=json.dumps(data_dict),
+                                 headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"failed to extend sandbox {sandbox_id}"))
+        return response.json()
+
+    def stop_sandbox(self, sandbox_id: str):
+        """ Stop the sandbox given sandbox id """
+        response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'"))
+        return response.json()
+
+    # SANDBOX GET REQUESTS
+    def get_sandboxes(self):
+        """Get list of sandboxes
+        :return:
+        """
+        response = requests.get(f'{self._versioned_url}/sandboxes', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox list"))
+        return response.json()
+
+    def get_sandbox_details(self, sandbox_id: str):
+        """ Get details of the given sandbox id """
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for {sandbox_id}"))
+        return response.json()
+
+    def get_sandbox_activity(self, sandbox_id: str):
+        """ Get list of sandbox activity """
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/activity', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for {sandbox_id}"))
+        return response.json()
+
+    def get_sandbox_commands(self, sandbox_id: str):
+        """ Get list of sandbox commands """
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for {sandbox_id}"))
+        return response.json()
+
+    def get_sandbox_command_details(self, sandbox_id: str, command_name: str):
+        """ Get details of specific sandbox command """
+        response = requests.get('{}/v2/sandboxes/{}/commands/{}'.format(
+            self._base_url, sandbox_id, command_name),
+            headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def get_sandbox_components(self, sandbox_id):
+        """Get list of sandbox components
+        :param str sandbox_id: Sandbox id
+        :return: Returns a list of tuples of component type, name and id
+        """
+        response = requests.get('{}/v2/sandboxes/{}/components'.format(
+            self._base_url, sandbox_id), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def get_sandbox_component_details(self, sandbox_id, component_id):
+        """Get details of components in sandbox
+        :param str sandbox_id: Sandbox id
+        :param str component_id: Component id
+        :return: Returns a tuple of component type, name, id, address, description
+        """
+        response = requests.get('{}/v2/sandboxes/{}/components/{}'.format(self._base_url, sandbox_id, component_id),
+                                headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def get_sandbox_component_commands(self, sandbox_id, component_id):
+        """Get list of commands for a particular component in sandbox
+        :param str sandbox_id: Sandbox id
+        :param str component_id: Component id
+        :return:
+        """
+        response = requests.get('{}/v2/sandboxes/{}/components/{}/commands'.format(
+            self._base_url, sandbox_id, component_id), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def get_sandbox_component_command_details(self, sandbox_id, component_id, command):
+        """Get details of a command of sandbox component
+        :param str sandbox_id: Sandbox id
+        :param str component_id: Component id
+        :param str command: Command name
+        :return:
+        """
+        response = requests.get('{}/v2/sandboxes/{}/components/{}/commands/{}'.format(
+            self._base_url, sandbox_id, component_id, command), headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    def get_sandbox_output(self, sandbox_id):
+        """Get list of sandbox output
+        :param str sandbox_id: Sandbox id
+        :return:
+        """
+        response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._base_url, sandbox_id, 'output'),
+                                headers=self._headers)
+        if response.ok:
+            return json.loads(response.content)
+        return response.reason
+
+    # BLUEPRINT GET REQUESTS
+    def get_blueprints(self):
+        """ Get list of blueprints """
+        response = requests.get(f'{self._versioned_url}/blueprints', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, "Failed to get blueprints"))
+        return response.json()
+
+    def get_blueprint_details(self, blueprint_id: str):
+        """
+        Get details of a specific blueprint
+        Can pass either blueprint name OR blueprint ID
+        """
+        response = requests.get(f'{self._versioned_url}/blueprints/{blueprint_id}')
+        if not response.ok:
+            raise SandboxRestException(
+                self._format_err(response, f"Failed to get blueprint data for {blueprint_id}"))
+        return response.json()
+
+
+usage = """
+    bp_name = 'Environment1'
+    bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa'
+    sb_duration = 'PT10M'
+
+    sb_cmd = 'blueprint_power_cycle'
+    sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True}
+    sb_component = 'Demo Resource 1'
+    sb_component_cmd = 'Hello'
+    sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
+
+    sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global')
+    r = sandbox_api.get_blueprints()
+    print(r)
+    r = sandbox_api.get_blueprint_details(bp_name)
+    print(r)
+    r = sandbox_api.get_blueprint_details(bp_id)
+    print(r)
+    r = sandbox_api.start_sandbox(bp_name, sb_duration)
+    print(r)
+    sb_id = r['id']
+    r = sandbox_api.get_sandboxes()
+    print(r)
+    r = sandbox_api.get_sandbox_details(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_commands(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup')
+    print(r)
+    r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params)
+    print(r)
+    r = sandbox_api.get_sandbox_components(sb_id)
+    print(r)
+    comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0]
+
+    r = sandbox_api.get_sandbox_component_details(sb_id, comp_id)
+    print(r)
+    r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id)
+    print(r)
+    r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd)
+    print(r)
+    r = sandbox_api.sandbox_component_command_start(
+        sb_id, comp_id, sb_component_cmd,
+        params=sb_comp_cmd_params)
+    print(r)
+    r = sandbox_api.extend_sandbox(sb_id, sb_duration)
+    print(r)
+    r = sandbox_api.get_sandbox_activity(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_output(sb_id)
+    print(r)
+    print(sandbox_api.stop_sandbox(sb_id))
+"""
+
+
+def main():
+    bp_name = 'Environment1'
+    bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa'
+    sb_duration = 'PT10M'
+
+    sb_cmd = 'blueprint_power_cycle'
+    sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True}
+    sb_component = 'Demo Resource 1'
+    sb_component_cmd = 'Hello'
+    sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
+
+    sandbox_api = SandboxRestApiClient('localhost', 'admin', 'admin', 'Global')
+    r = sandbox_api.get_blueprints()
+    print(r)
+    r = sandbox_api.get_blueprint_details(bp_name)
+    print(r)
+    r = sandbox_api.get_blueprint_details(bp_id)
+    print(r)
+    r = sandbox_api.start_sandbox(bp_name, sb_duration)
+    print(r)
+    sb_id = r['id']
+    r = sandbox_api.get_sandboxes()
+    print(r)
+    r = sandbox_api.get_sandbox_details(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_commands(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup')
+    print(r)
+    r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params)
+    print(r)
+    r = sandbox_api.get_sandbox_components(sb_id)
+    print(r)
+    comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0]
+
+    r = sandbox_api.get_sandbox_component_details(sb_id, comp_id)
+    print(r)
+    r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id)
+    print(r)
+    r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd)
+    print(r)
+    r = sandbox_api.sandbox_component_command_start(
+        sb_id, comp_id, sb_component_cmd,
+        params=sb_comp_cmd_params)
+    print(r)
+    r = sandbox_api.extend_sandbox(sb_id, sb_duration)
+    print(r)
+    r = sandbox_api.get_sandbox_activity(sb_id)
+    print(r)
+    r = sandbox_api.get_sandbox_output(sb_id)
+    print(r)
+    print(sandbox_api.stop_sandbox(sb_id))
+
+
+if __name__ == '__main__':
+    # print(usage)
+    # main()
+    api = SandboxRestApiClient("localhost", "admin", "admin")
+    bps = api.get_blueprints()
+    pass
diff --git a/setup.py b/setup.py
index 3740fdc..919634e 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,6 @@ def readme():
     classifiers=[
         'License :: OSI Approved :: Apache Software License',
         'Natural Language :: English',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
     ],

From be7d44930e77d80059e52902ee31188205f8ea7c Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Sat, 2 Oct 2021 15:22:13 +0300
Subject: [PATCH 08/47] refactored folder structure

---
 {sandbox_api => src/sandbox_api}/__init__.py  |   0
 .../sandbox_api}/sandbox_rest.py              | 341 ++++++++----------
 2 files changed, 145 insertions(+), 196 deletions(-)
 rename {sandbox_api => src/sandbox_api}/__init__.py (100%)
 rename {sandbox_api => src/sandbox_api}/sandbox_rest.py (51%)

diff --git a/sandbox_api/__init__.py b/src/sandbox_api/__init__.py
similarity index 100%
rename from sandbox_api/__init__.py
rename to src/sandbox_api/__init__.py
diff --git a/sandbox_api/sandbox_rest.py b/src/sandbox_api/sandbox_rest.py
similarity index 51%
rename from sandbox_api/sandbox_rest.py
rename to src/sandbox_api/sandbox_rest.py
index f100dd8..0ad5dd7 100644
--- a/sandbox_api/sandbox_rest.py
+++ b/src/sandbox_api/sandbox_rest.py
@@ -6,17 +6,22 @@
 
 
 class SandboxRestException(Exception):
+    """ General exception to raise inside Rest client class """
     pass
 
 
 @dataclass
 class InputParam:
+    """ To model the param objects passed to sandbox / component command endpoints """
     name: str
     value: str
 
 
 class SandboxRestApiClient:
-    """ Python wrapper for CloudShell Sandbox API """
+    """
+    Python wrapper for CloudShell Sandbox API
+    View http://<API_SERVER>/api/v2/explore to see schemas of return json values
+    """
 
     def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False,
                  api_version="v2"):
@@ -74,7 +79,7 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
                                  headers=self._auth_headers,
                                  data=json.dumps(data_dict))
         if not response.ok:
-            err_msg = self._format_err(response, f"Failed to start sandbox from blueprint{blueprint_id}")
+            err_msg = self._format_err(response, f"Failed to start sandbox from blueprint '{blueprint_id}'")
             raise SandboxRestException(err_msg)
         return response.json()
 
@@ -96,32 +101,32 @@ def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params
                                  headers=self._auth_headers,
                                  data=json.dumps(data_dict))
         if not response.ok:
-            err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint{blueprint_id}")
+            err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'")
             raise SandboxRestException(err_msg)
         return response.json()
 
-    def start_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None,
-                              print_output=True):
+    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None,
+                            print_output=True):
         """ Run a sandbox level command """
         url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start'
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
-        response = requests.post(url, data=json.dumps(data_dict),headers=self._auth_headers)
+        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to start sandbox command {command_name}"))
+            raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'"))
         return response.json()
 
-    def start_component_command(self, sandbox_id: str, component_id: str, command_name: str,
-                                params: List[InputParam] = None, print_output: bool = True):
+    def run_component_command(self, sandbox_id: str, component_id: str, command_name: str,
+                              params: List[InputParam] = None, print_output: bool = True):
         """ Start a command on sandbox component """
         url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start'
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
-        response = requests.post(url, data=json.dumps(data_dict),headers=self._auth_headers)
+        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to start component command {command_name}"))
+            raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'"))
         return response.json()
 
     def extend_sandbox(self, sandbox_id: str, duration: str):
@@ -135,7 +140,7 @@ def extend_sandbox(self, sandbox_id: str, duration: str):
                                  data=json.dumps(data_dict),
                                  headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to extend sandbox {sandbox_id}"))
+            raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'"))
         return response.json()
 
     def stop_sandbox(self, sandbox_id: str):
@@ -146,11 +151,12 @@ def stop_sandbox(self, sandbox_id: str):
         return response.json()
 
     # SANDBOX GET REQUESTS
-    def get_sandboxes(self):
+    def get_sandboxes(self, show_historic=False):
         """Get list of sandboxes
         :return:
         """
-        response = requests.get(f'{self._versioned_url}/sandboxes', headers=self._auth_headers)
+        params = {"show_historic": show_historic}
+        response = requests.get(f'{self._versioned_url}/sandboxes', headers=self._auth_headers, params=params)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"Failed to get sandbox list"))
         return response.json()
@@ -159,90 +165,129 @@ def get_sandbox_details(self, sandbox_id: str):
         """ Get details of the given sandbox id """
         response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}', headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for {sandbox_id}"))
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'"))
         return response.json()
 
-    def get_sandbox_activity(self, sandbox_id: str):
-        """ Get list of sandbox activity """
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/activity', headers=self._auth_headers)
+    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None,
+                             tail: int = None):
+        """
+        Get list of sandbox activity
+        'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
+        'from_event_id' - integer id of event where to start pulling results from
+        'tail' - how many of the last entries you want to pull
+        'error_only' - to filter for error events only
+        """
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/activity'
+        params = {}
+
+        if error_only:
+            params["error_only"] = error_only
+        if since:
+            params["since"] = since
+        if from_event_id:
+            params["from_event_id"] = from_event_id
+        if tail:
+            params["tail"] = tail
+
+        if params:
+            response = requests.get(url, headers=self._auth_headers, params=params)
+        else:
+            response = requests.get(url, headers=self._auth_headers)
+
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for {sandbox_id}"))
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for '{sandbox_id}'"))
         return response.json()
 
     def get_sandbox_commands(self, sandbox_id: str):
         """ Get list of sandbox commands """
         response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for {sandbox_id}"))
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'"))
         return response.json()
 
     def get_sandbox_command_details(self, sandbox_id: str, command_name: str):
         """ Get details of specific sandbox command """
-        response = requests.get('{}/v2/sandboxes/{}/commands/{}'.format(
-            self._base_url, sandbox_id, command_name),
-            headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_components(self, sandbox_id):
-        """Get list of sandbox components
-        :param str sandbox_id: Sandbox id
-        :return: Returns a list of tuples of component type, name and id
-        """
-        response = requests.get('{}/v2/sandboxes/{}/components'.format(
-            self._base_url, sandbox_id), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_component_details(self, sandbox_id, component_id):
-        """Get details of components in sandbox
-        :param str sandbox_id: Sandbox id
-        :param str component_id: Component id
-        :return: Returns a tuple of component type, name, id, address, description
-        """
-        response = requests.get('{}/v2/sandboxes/{}/components/{}'.format(self._base_url, sandbox_id, component_id),
-                                headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_component_commands(self, sandbox_id, component_id):
-        """Get list of commands for a particular component in sandbox
-        :param str sandbox_id: Sandbox id
-        :param str component_id: Component id
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/components/{}/commands'.format(
-            self._base_url, sandbox_id, component_id), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_component_command_details(self, sandbox_id, component_id, command):
-        """Get details of a command of sandbox component
-        :param str sandbox_id: Sandbox id
-        :param str component_id: Component id
-        :param str command: Command name
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/components/{}/commands/{}'.format(
-            self._base_url, sandbox_id, component_id, command), headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
-
-    def get_sandbox_output(self, sandbox_id):
-        """Get list of sandbox output
-        :param str sandbox_id: Sandbox id
-        :return:
-        """
-        response = requests.get('{}/v2/sandboxes/{}/{}'.format(self._base_url, sandbox_id, 'output'),
-                                headers=self._headers)
-        if response.ok:
-            return json.loads(response.content)
-        return response.reason
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}',
+                                headers=self._auth_headers)
+        if not response.ok:
+            err_msg = self._format_err(response,
+                                       f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_components(self, sandbox_id: str):
+        """ Get list of sandbox components """
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components', headers=self._auth_headers)
+        if not response.ok:
+            err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_component_details(self, sandbox_id: str, component_id: str):
+        """ Get details of components in sandbox """
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}',
+                                headers=self._auth_headers)
+        if not response.ok:
+            custom_err_msg = (f"Failed to get sandbox component details for component: '{component_id}', "
+                              f"sandbox: '{sandbox_id}'")
+            err_msg = self._format_err(response, custom_err_msg)
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_component_commands(self, sandbox_id: str, component_id: str):
+        """ Get list of commands for a particular component in sandbox """
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands',
+                                headers=self._auth_headers)
+        if not response.ok:
+            custom_err_msg = (f"Failed to get sandbox component commands list for component: '{component_id}', "
+                              f"sandbox: '{sandbox_id}'")
+            err_msg = self._format_err(response, custom_err_msg)
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str):
+        """ Get details of a command of sandbox component """
+        response = requests.get(
+            f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}',
+            headers=self._auth_headers)
+        if not response.ok:
+            custom_err_msg = (f"Failed to get sandbox component command details for component: '{component_id}', "
+                              f"sandbox: '{sandbox_id}'")
+            err_msg = self._format_err(response, custom_err_msg)
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_instructions(self, sandbox_id: str):
+        """ pull the instruction text of sandbox """
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions',
+                                headers=self._auth_headers)
+        if not response.ok:
+            err_msg = self._format_err(response,
+                                       f"Failed to get sandbox instructions for '{sandbox_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None):
+        """ Get list of sandbox output """
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions'
+        params = {}
+        if tail:
+            params["tail"] = tail
+        if from_entry_id:
+            params["from_entry_id"] = from_entry_id
+        if since:
+            params["since"] = since
+
+        if params:
+            response = requests.get(url, headers=self._auth_headers, params=params)
+        else:
+            response = requests.get(url, headers=self._auth_headers)
+
+        if not response.ok:
+            err_msg = self._format_err(response,
+                                       f"Failed to get sandbox instructions for '{sandbox_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
 
     # BLUEPRINT GET REQUESTS
     def get_blueprints(self):
@@ -257,125 +302,29 @@ def get_blueprint_details(self, blueprint_id: str):
         Get details of a specific blueprint
         Can pass either blueprint name OR blueprint ID
         """
-        response = requests.get(f'{self._versioned_url}/blueprints/{blueprint_id}')
+        response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'")
         if not response.ok:
             raise SandboxRestException(
-                self._format_err(response, f"Failed to get blueprint data for {blueprint_id}"))
+                self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'"))
         return response.json()
 
+    # EXECUTIONS
+    def get_execution_details(self, execution_id: str):
+        response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(
+                self._format_err(response, f"Failed to get execution details for '{execution_id}'"))
+        return response.json()
 
-usage = """
-    bp_name = 'Environment1'
-    bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa'
-    sb_duration = 'PT10M'
-
-    sb_cmd = 'blueprint_power_cycle'
-    sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True}
-    sb_component = 'Demo Resource 1'
-    sb_component_cmd = 'Hello'
-    sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
-
-    sandbox_api = SandboxAPI('localhost', 'admin', 'admin', 'Global')
-    r = sandbox_api.get_blueprints()
-    print(r)
-    r = sandbox_api.get_blueprint_details(bp_name)
-    print(r)
-    r = sandbox_api.get_blueprint_details(bp_id)
-    print(r)
-    r = sandbox_api.start_sandbox(bp_name, sb_duration)
-    print(r)
-    sb_id = r['id']
-    r = sandbox_api.get_sandboxes()
-    print(r)
-    r = sandbox_api.get_sandbox_details(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_commands(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup')
-    print(r)
-    r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params)
-    print(r)
-    r = sandbox_api.get_sandbox_components(sb_id)
-    print(r)
-    comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0]
-
-    r = sandbox_api.get_sandbox_component_details(sb_id, comp_id)
-    print(r)
-    r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id)
-    print(r)
-    r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd)
-    print(r)
-    r = sandbox_api.sandbox_component_command_start(
-        sb_id, comp_id, sb_component_cmd,
-        params=sb_comp_cmd_params)
-    print(r)
-    r = sandbox_api.extend_sandbox(sb_id, sb_duration)
-    print(r)
-    r = sandbox_api.get_sandbox_activity(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_output(sb_id)
-    print(r)
-    print(sandbox_api.stop_sandbox(sb_id))
-"""
-
-
-def main():
-    bp_name = 'Environment1'
-    bp_id = '9a3ad040-14ff-4078-9b0a-6ce7986daaaa'
-    sb_duration = 'PT10M'
-
-    sb_cmd = 'blueprint_power_cycle'
-    sb_cmd_params = {"params": [{"name": "WaitTime", "value": "1"}], "printOutput": True}
-    sb_component = 'Demo Resource 1'
-    sb_component_cmd = 'Hello'
-    sb_comp_cmd_params = {"params": [{"name": "Duration", "value": "Sandbox tester"}], "printOutput": True}
-
-    sandbox_api = SandboxRestApiClient('localhost', 'admin', 'admin', 'Global')
-    r = sandbox_api.get_blueprints()
-    print(r)
-    r = sandbox_api.get_blueprint_details(bp_name)
-    print(r)
-    r = sandbox_api.get_blueprint_details(bp_id)
-    print(r)
-    r = sandbox_api.start_sandbox(bp_name, sb_duration)
-    print(r)
-    sb_id = r['id']
-    r = sandbox_api.get_sandboxes()
-    print(r)
-    r = sandbox_api.get_sandbox_details(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_commands(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_command_details(sb_id, 'Setup')
-    print(r)
-    r = sandbox_api.sandbox_command_start(sb_id, sb_cmd, sb_cmd_params)
-    print(r)
-    r = sandbox_api.get_sandbox_components(sb_id)
-    print(r)
-    comp_id = [c.get('id') for c in r if c.get('name') == sb_component][0]
-
-    r = sandbox_api.get_sandbox_component_details(sb_id, comp_id)
-    print(r)
-    r = sandbox_api.get_sandbox_component_commands(sb_id, comp_id)
-    print(r)
-    r = sandbox_api.get_sandbox_component_command_details(sb_id, comp_id, sb_component_cmd)
-    print(r)
-    r = sandbox_api.sandbox_component_command_start(
-        sb_id, comp_id, sb_component_cmd,
-        params=sb_comp_cmd_params)
-    print(r)
-    r = sandbox_api.extend_sandbox(sb_id, sb_duration)
-    print(r)
-    r = sandbox_api.get_sandbox_activity(sb_id)
-    print(r)
-    r = sandbox_api.get_sandbox_output(sb_id)
-    print(r)
-    print(sandbox_api.stop_sandbox(sb_id))
+    def delete_execution(self, execution_id: str):
+        response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(
+                self._format_err(response, f"Failed to delete execution for '{execution_id}'"))
+        return response.json()
 
 
 if __name__ == '__main__':
-    # print(usage)
-    # main()
     api = SandboxRestApiClient("localhost", "admin", "admin")
     bps = api.get_blueprints()
     pass

From 23475df86bc5be748c8bfc921a503190332ce3c6 Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Wed, 6 Oct 2021 00:12:59 +0100
Subject: [PATCH 09/47] added auth methods

---
 .gitignore                                    |   4 +-
 build/lib/cloudshell/__init__.py              |   3 +
 build/lib/cloudshell/sandbox_rest/__init__.py |   3 +
 .../cloudshell/sandbox_rest/flow_helpers.py   |   0
 .../cloudshell/sandbox_rest/sandbox_api.py    |  50 +--
 cloudshell/__init__.py                        |   3 +
 cloudshell/sandbox_rest/__init__.py           |   3 +
 .../__pycache__/__init__.cpython-37.pyc       | Bin 0 -> 283 bytes
 .../__pycache__/__init__.cpython-39.pyc       | Bin 0 -> 287 bytes
 .../__pycache__/exceptions.cpython-37.pyc     | Bin 0 -> 802 bytes
 .../__pycache__/sandbox_api.cpython-39.pyc    | Bin 0 -> 13066 bytes
 cloudshell/sandbox_rest/exceptions.py         |  13 +
 cloudshell/sandbox_rest/flow_helpers.py       |   0
 cloudshell/sandbox_rest/sandbox_api.py        | 419 ++++++++++++++++++
 .../PKG-INFO                                  |  41 ++
 .../SOURCES.txt                               |  11 +
 .../dependency_links.txt                      |   1 +
 .../requires.txt                              |   1 +
 .../top_level.txt                             |   1 +
 ...cloudshell_sandboxapi_wrapper-2.0.0.tar.gz | Bin 0 -> 5103 bytes
 setup.py                                      |  35 +-
 test-requirements.txt                         |   1 +
 tests/test_rest_client.py                     |  51 +++
 version.txt                                   |   1 +
 24 files changed, 599 insertions(+), 42 deletions(-)
 create mode 100644 build/lib/cloudshell/__init__.py
 create mode 100644 build/lib/cloudshell/sandbox_rest/__init__.py
 rename src/sandbox_api/__init__.py => build/lib/cloudshell/sandbox_rest/flow_helpers.py (100%)
 rename src/sandbox_api/sandbox_rest.py => build/lib/cloudshell/sandbox_rest/sandbox_api.py (90%)
 create mode 100644 cloudshell/__init__.py
 create mode 100644 cloudshell/sandbox_rest/__init__.py
 create mode 100644 cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc
 create mode 100644 cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc
 create mode 100644 cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc
 create mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc
 create mode 100644 cloudshell/sandbox_rest/exceptions.py
 create mode 100644 cloudshell/sandbox_rest/flow_helpers.py
 create mode 100644 cloudshell/sandbox_rest/sandbox_api.py
 create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
 create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
 create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
 create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/requires.txt
 create mode 100644 cloudshell_sandboxapi_wrapper.egg-info/top_level.txt
 create mode 100644 dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz
 create mode 100644 test-requirements.txt
 create mode 100644 tests/test_rest_client.py
 create mode 100644 version.txt

diff --git a/.gitignore b/.gitignore
index 62c8935..8750fc0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
-.idea/
\ No newline at end of file
+.idea/
+.pytest_cache/
+*.egg
\ No newline at end of file
diff --git a/build/lib/cloudshell/__init__.py b/build/lib/cloudshell/__init__.py
new file mode 100644
index 0000000..b50dc4a
--- /dev/null
+++ b/build/lib/cloudshell/__init__.py
@@ -0,0 +1,3 @@
+__author__ = 'quali'
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/build/lib/cloudshell/sandbox_rest/__init__.py b/build/lib/cloudshell/sandbox_rest/__init__.py
new file mode 100644
index 0000000..b50dc4a
--- /dev/null
+++ b/build/lib/cloudshell/sandbox_rest/__init__.py
@@ -0,0 +1,3 @@
+__author__ = 'quali'
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/src/sandbox_api/__init__.py b/build/lib/cloudshell/sandbox_rest/flow_helpers.py
similarity index 100%
rename from src/sandbox_api/__init__.py
rename to build/lib/cloudshell/sandbox_rest/flow_helpers.py
diff --git a/src/sandbox_api/sandbox_rest.py b/build/lib/cloudshell/sandbox_rest/sandbox_api.py
similarity index 90%
rename from src/sandbox_api/sandbox_rest.py
rename to build/lib/cloudshell/sandbox_rest/sandbox_api.py
index 0ad5dd7..ecc62ef 100644
--- a/src/sandbox_api/sandbox_rest.py
+++ b/build/lib/cloudshell/sandbox_rest/sandbox_api.py
@@ -25,11 +25,10 @@ class SandboxRestApiClient:
 
     def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False,
                  api_version="v2"):
-        """ logs into api and sets headers for future requests """
+        """ login to api and store headers for future requests """
         protocol = "https" if is_https else "http"
         self._base_url = f"{protocol}://{host}:{port}/api"
         self._versioned_url = f"{self._base_url}/{api_version}"
-        self._session = requests.Session()
         self._auth_headers = self._get_auth_headers(username, password, domain)
 
     def _get_auth_headers(self, user_name: str, password: str, domain: str):
@@ -65,19 +64,17 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
         Create a sandbox from the provided blueprint id
         Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
         """
-        # sandbox name will default to blueprint name if not passed
-        if not sandbox_name:
-            sandbox_name = self.get_blueprint_details(blueprint_id)['name']
-
-        data_dict = {"duration": duration, "name": sandbox_name}
-        if permitted_users:
-            data_dict['permitted_users'] = permitted_users
-        if bp_params:
-            data_dict["params"] = [asdict(x) for x in bp_params]
-
-        response = requests.post(f'{self._versioned_url}/blueprints/{blueprint_id}/start',
-                                 headers=self._auth_headers,
-                                 data=json.dumps(data_dict))
+        url = f'{self._versioned_url}/blueprints/{blueprint_id}/start'
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name']
+
+        data_dict = {
+            "duration": duration,
+            "name": sandbox_name,
+            "permitted_users": permitted_users if permitted_users else [],
+            "params": [asdict(x) for x in bp_params] if bp_params else []
+        }
+
+        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
         if not response.ok:
             err_msg = self._format_err(response, f"Failed to start sandbox from blueprint '{blueprint_id}'")
             raise SandboxRestException(err_msg)
@@ -86,20 +83,16 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
     def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None,
                                  permitted_users: List[str] = None):
         """ Create a persistent sandbox from the provided blueprint id """
+        url = f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent'
 
-        # sandbox name will default to blueprint name if not passed
-        if not sandbox_name:
-            sandbox_name = self.get_blueprint_details(blueprint_id)['name']
-
-        data_dict = {"name": sandbox_name}
-        if permitted_users:
-            data_dict['permitted_users'] = permitted_users
-        if bp_params:
-            data_dict["params"] = [asdict(x) for x in bp_params]
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name']
+        data_dict = {
+            "name": sandbox_name,
+            "permitted_users": permitted_users if permitted_users else [],
+            "params": [asdict(x) for x in bp_params] if bp_params else []
+        }
 
-        response = requests.post(f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent',
-                                 headers=self._auth_headers,
-                                 data=json.dumps(data_dict))
+        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
         if not response.ok:
             err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'")
             raise SandboxRestException(err_msg)
@@ -126,7 +119,8 @@ def run_component_command(self, sandbox_id: str, component_id: str, command_name
         data_dict["params"] = params
         response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'"))
+            raise SandboxRestException(
+                self._format_err(response, f"failed to start component command '{command_name}'"))
         return response.json()
 
     def extend_sandbox(self, sandbox_id: str, duration: str):
diff --git a/cloudshell/__init__.py b/cloudshell/__init__.py
new file mode 100644
index 0000000..b50dc4a
--- /dev/null
+++ b/cloudshell/__init__.py
@@ -0,0 +1,3 @@
+__author__ = 'quali'
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/cloudshell/sandbox_rest/__init__.py b/cloudshell/sandbox_rest/__init__.py
new file mode 100644
index 0000000..b50dc4a
--- /dev/null
+++ b/cloudshell/sandbox_rest/__init__.py
@@ -0,0 +1,3 @@
+__author__ = 'quali'
+from pkgutil import extend_path
+__path__ = extend_path(__path__, __name__)
\ No newline at end of file
diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..16c0d26a19c161f66f05ab525034462bea0f8f07
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..25178204b2e5b5ce4876c516480741b828ffc7da
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..178f54132054515b69782940d859da31f7c48a9c
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2eef5552e21c5b945ca34ec9e8843b988fe1aa3b
GIT binary patch
diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py
new file mode 100644
index 0000000..4b3a7ac
--- /dev/null
+++ b/cloudshell/sandbox_rest/exceptions.py
@@ -0,0 +1,13 @@
+class SandboxRestException(Exception):
+    """ General exception to raise inside Rest client class """
+    pass
+
+
+class SandboxRestAuthException(Exception):
+    """ Failed auth actions """
+    pass
+
+
+class SandboxRestInitException(ValueError):
+    """ Failed auth actions """
+    pass
diff --git a/cloudshell/sandbox_rest/flow_helpers.py b/cloudshell/sandbox_rest/flow_helpers.py
new file mode 100644
index 0000000..e69de29
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
new file mode 100644
index 0000000..3c99893
--- /dev/null
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -0,0 +1,419 @@
+import json
+import time
+from typing import List
+import requests
+from dataclasses import dataclass, asdict
+from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException
+
+
+@dataclass
+class InputParam:
+    """
+    param objects passed to sandbox / component command endpoints
+    sandbox global inputs, commands and resource commands all follow this generic name/value convention
+    """
+    name: str
+    value: str
+
+
+class SandboxRestApiClient:
+    """
+    Python wrapper for CloudShell Sandbox API
+    View http://<API_SERVER>/api/v2/explore to see schemas of return json values
+    """
+
+    def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False,
+                 api_version="v2"):
+        """ login to api and store headers for future requests """
+        _protocol = "https" if is_https else "http"
+        self.host = host
+        self.username = username
+        self._password = password
+        self.domain = domain
+        self._base_url = f"{_protocol}://{host}:{port}/api"
+        self._versioned_url = f"{self._base_url}/{api_version}"
+        self.auth_token = self.get_token_with_credentials(username, password, domain)
+        self._auth_headers = self._build_auth_headers(self.auth_token)
+
+    # CONTEXT MANAGER METHODS
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        # invalidate stored api token on exit
+        self.invalidate_auth()
+
+    @staticmethod
+    def _build_auth_headers(login_token: str) -> dict:
+        """
+        interpolate token into auth_headers dict
+        """
+        auth_headers = {
+            'Authorization': f'Basic {login_token}',
+            'Content-Type': 'application/json'
+        }
+        return auth_headers
+
+    def refresh_auth(self):
+        self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain)
+        self._auth_headers = self._build_auth_headers(self.auth_token)
+
+    def invalidate_auth(self):
+        self.invalidate_target_token(self.auth_token)
+        self._auth_headers = None
+
+    def _validate_auth_headers(self):
+        if not self._auth_headers:
+            raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session")
+
+    # LOGIN METHODS
+    def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str:
+        """
+       Get token from credentials - extraneous quotes stripped off token string
+       """
+        login_res = requests.put(url=f"{self._base_url}/login",
+                                 data=json.dumps({"username": user_name, "password": password, "domain": domain}),
+                                 headers={"Content-Type": "application/json"})
+
+        if not login_res.ok:
+            raise SandboxRestAuthException(self._format_err(login_res, "Failed Login"))
+
+        login_token = login_res.text[1:-1]
+        return login_token
+
+    def get_token_for_target_user(self, user_name: str) -> str:
+        """
+        Get token for target user - remove extraneous quotes
+        """
+        self._validate_auth_headers()
+        login_res = requests.post(url=f"{self._base_url}/token",
+                                  data=json.dumps({"username": user_name}),
+                                  headers=self._auth_headers)
+
+        if not login_res.ok:
+            raise SandboxRestException(self._format_err(login_res, f"Failed to get get token for user {user_name}"))
+
+        login_token = login_res.text[1:-1]
+        return login_token
+
+    def invalidate_target_token(self, token_id: str) -> None:
+        self._validate_auth_headers()
+        login_res = requests.delete(url=f"{self._base_url}/token/{token_id}",
+                                    headers=self._auth_headers)
+        if not login_res.ok:
+            raise SandboxRestException(self._format_err(login_res, "Failed to delete token"))
+
+    def _format_err(self, response: requests.Response, custom_err_msg="Failed Api Call"):
+        response_data = (f"Response: {response.status_code}, Reason: {response.reason}\n"
+                         f"Request URL: {response.request.url}\n"
+                         f"Request Headers: {response.request.headers}")
+        return (f"Sandbox API Error for User: '{self.username}', Domain: '{self.domain}'.\n"
+                f"{custom_err_msg}\n"
+                f"{response_data}")
+
+    # SANDBOX POST REQUESTS
+    def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
+                      bp_params: List[InputParam] = None, permitted_users: List[str] = None):
+        """
+        Create a sandbox from the provided blueprint id
+        Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
+        """
+        self._validate_auth_headers()
+        url = f'{self._versioned_url}/blueprints/{blueprint_id}/start'
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name']
+
+        data_dict = {
+            "duration": duration,
+            "name": sandbox_name,
+            "permitted_users": permitted_users if permitted_users else [],
+            "params": [asdict(x) for x in bp_params] if bp_params else []
+        }
+
+        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
+        if not response.ok:
+            custom_msg = f"Failed to start '{blueprint_id}'. "
+            if 400 <= response.status_code < 500:
+                custom_msg += "Ensure blueprint is 'Public' and in correct domain."
+            if response.status_code >= 500:
+                custom_msg += "Internal Server 500 Error During Request."
+            err_msg = self._format_err(response, custom_msg)
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None,
+                                 permitted_users: List[str] = None):
+        """ Create a persistent sandbox from the provided blueprint id """
+        self._validate_auth_headers()
+        url = f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent'
+
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name']
+        data_dict = {
+            "name": sandbox_name,
+            "permitted_users": permitted_users if permitted_users else [],
+            "params": [asdict(x) for x in bp_params] if bp_params else []
+        }
+
+        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
+        if not response.ok:
+            err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None,
+                            print_output=True):
+        """ Run a sandbox level command """
+        self._validate_auth_headers()
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start'
+        data_dict = {"printOutput": print_output}
+        params = [asdict(x) for x in params] if params else []
+        data_dict["params"] = params
+        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'"))
+        return response.json()
+
+    def run_component_command(self, sandbox_id: str, component_id: str, command_name: str,
+                              params: List[InputParam] = None, print_output: bool = True):
+        """ Start a command on sandbox component """
+        self._validate_auth_headers()
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start'
+        data_dict = {"printOutput": print_output}
+        params = [asdict(x) for x in params] if params else []
+        data_dict["params"] = params
+        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(
+                self._format_err(response, f"failed to start component command '{command_name}'"))
+        return response.json()
+
+    def extend_sandbox(self, sandbox_id: str, duration: str):
+        """Extend the sandbox
+        :param str sandbox_id: Sandbox id
+        :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
+        :return:
+        """
+        self._validate_auth_headers()
+        data_dict = {"extended_time": duration}
+        response = requests.post(f'{self._base_url}/sandboxes/{sandbox_id}/extend',
+                                 data=json.dumps(data_dict),
+                                 headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'"))
+        return response.json()
+
+    def stop_sandbox(self, sandbox_id: str):
+        """ Stop the sandbox given sandbox id """
+        self._validate_auth_headers()
+        response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'"))
+        return response.json()
+
+    # SANDBOX GET REQUESTS
+    def get_sandboxes(self, show_historic=False):
+        """ Get list of sandboxes """
+        self._validate_auth_headers()
+        url = f'{self._versioned_url}/sandboxes'
+        params = {"show_historic": "true" if show_historic else "false"}
+        response = requests.get(url, headers=self._auth_headers, params=params)
+        if not response.ok:
+            err_msg = self._format_err(response, f"Failed to get sandbox list")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_details(self, sandbox_id: str):
+        """ Get details of the given sandbox id """
+        self._validate_auth_headers()
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}'
+        response = requests.get(url, headers=self._auth_headers)
+        if not response.ok:
+            exc_msg = self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'")
+            raise SandboxRestException(exc_msg)
+        return response.json()
+
+    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None,
+                             tail: int = None):
+        """
+        Get list of sandbox activity
+        'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
+        'from_event_id' - integer id of event where to start pulling results from
+        'tail' - how many of the last entries you want to pull
+        'error_only' - to filter for error events only
+        """
+        self._validate_auth_headers()
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/activity'
+        params = {}
+
+        if error_only:
+            params["error_only"] = error_only
+        if since:
+            params["since"] = since
+        if from_event_id:
+            params["from_event_id"] = from_event_id
+        if tail:
+            params["tail"] = tail
+
+        if params:
+            response = requests.get(url, headers=self._auth_headers, params=params)
+        else:
+            response = requests.get(url, headers=self._auth_headers)
+
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for '{sandbox_id}'"))
+        return response.json()
+
+    def get_sandbox_commands(self, sandbox_id: str):
+        """ Get list of sandbox commands """
+        self._validate_auth_headers()
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'"))
+        return response.json()
+
+    def get_sandbox_command_details(self, sandbox_id: str, command_name: str):
+        """ Get details of specific sandbox command """
+        self._validate_auth_headers()
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}',
+                                headers=self._auth_headers)
+        if not response.ok:
+            err_msg = self._format_err(response,
+                                       f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_components(self, sandbox_id: str):
+        """ Get list of sandbox components """
+        self._validate_auth_headers()
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components', headers=self._auth_headers)
+        if not response.ok:
+            err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_component_details(self, sandbox_id: str, component_id: str):
+        """ Get details of components in sandbox """
+        self._validate_auth_headers()
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}',
+                                headers=self._auth_headers)
+        if not response.ok:
+            custom_err_msg = (f"Failed to get sandbox component details for component: '{component_id}', "
+                              f"sandbox: '{sandbox_id}'")
+            err_msg = self._format_err(response, custom_err_msg)
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_component_commands(self, sandbox_id: str, component_id: str):
+        """ Get list of commands for a particular component in sandbox """
+        self._validate_auth_headers()
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands',
+                                headers=self._auth_headers)
+        if not response.ok:
+            custom_err_msg = (f"Failed to get sandbox component commands list for component: '{component_id}', "
+                              f"sandbox: '{sandbox_id}'")
+            err_msg = self._format_err(response, custom_err_msg)
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str):
+        """ Get details of a command of sandbox component """
+        self._validate_auth_headers()
+        response = requests.get(
+            f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}',
+            headers=self._auth_headers)
+        if not response.ok:
+            custom_err_msg = (f"Failed to get sandbox component command details for component: '{component_id}', "
+                              f"sandbox: '{sandbox_id}'")
+            err_msg = self._format_err(response, custom_err_msg)
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_instructions(self, sandbox_id: str):
+        """ pull the instruction text of sandbox """
+        self._validate_auth_headers()
+        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions',
+                                headers=self._auth_headers)
+        if not response.ok:
+            err_msg = self._format_err(response,
+                                       f"Failed to get sandbox instructions for '{sandbox_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None):
+        """ Get list of sandbox output """
+        self._validate_auth_headers()
+        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions'
+        params = {}
+        if tail:
+            params["tail"] = tail
+        if from_entry_id:
+            params["from_entry_id"] = from_entry_id
+        if since:
+            params["since"] = since
+
+        if params:
+            response = requests.get(url, headers=self._auth_headers, params=params)
+        else:
+            response = requests.get(url, headers=self._auth_headers)
+
+        if not response.ok:
+            err_msg = self._format_err(response,
+                                       f"Failed to get sandbox instructions for '{sandbox_id}'")
+            raise SandboxRestException(err_msg)
+        return response.json()
+
+    # BLUEPRINT GET REQUESTS
+    def get_blueprints(self):
+        """ Get list of blueprints """
+        self._validate_auth_headers()
+        response = requests.get(f'{self._versioned_url}/blueprints', headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(self._format_err(response, "Failed to get blueprints"))
+        return response.json()
+
+    def get_blueprint_details(self, blueprint_id: str):
+        """
+        Get details of a specific blueprint
+        Can pass either blueprint name OR blueprint ID
+        """
+        self._validate_auth_headers()
+        response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'", headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(
+                self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'"))
+        return response.json()
+
+    # EXECUTIONS
+    def get_execution_details(self, execution_id: str):
+        self._validate_auth_headers()
+        response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(
+                self._format_err(response, f"Failed to get execution details for '{execution_id}'"))
+        return response.json()
+
+    def delete_execution(self, execution_id: str):
+        self._validate_auth_headers()
+        response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException(
+                self._format_err(response, f"Failed to delete execution for '{execution_id}'"))
+        return response.json()
+
+
+if __name__ == '__main__':
+    API_SERVER = "localhost"
+    ADMIN_USER = "admin"
+    ADMIN_PASS = "admin"
+
+    admin_api = SandboxRestApiClient(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS)
+    with admin_api:
+        # sample api call with admin user session to get all sandboxes
+        all_sandboxes = admin_api.get_sandboxes()
+        print("== List of sandboxes pulled by Admin ===")
+        print(json.dumps(all_sandboxes, indent=4))
+
+    admin_api.refresh_auth()
+    all_sandboxes = admin_api.get_sandboxes()
+    pass
+
+
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
new file mode 100644
index 0000000..df7ebb0
--- /dev/null
+++ b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
@@ -0,0 +1,41 @@
+Metadata-Version: 1.1
+Name: cloudshell-sandboxapi-wrapper
+Version: 2.0.0
+Summary: Python client for CloudShell Sandbox REST api - consume sandboxes via CI
+Home-page: https://github.com/QualiSystemsLab/Sandbox-API-Python
+Author: sadanand.s
+Author-email: sadanand.s@quali.com
+License: Apache 2.0
+Description: CloudShell Sandbox API Wrapper
+        ==============================
+        
+        Installation
+        *************
+        ::
+        
+            pip install cloudshell_sandboxapi_wrapper
+        
+        Example Usage
+        **************
+        ::
+        
+            from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI
+            sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT)
+            blueprints = sandbox.get_blueprints()
+            blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id']
+            sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME)
+            sandbox.stop_sandbox(sandbox_id)
+        
+        |
+        
+        :Note:
+          Tested on cloudshell 9.3 with Python 2.7/3.7/3.8.
+          For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API <https://help.quali.com/Online%20Help/9.3/Api-Guide/Content/API/CS-Snbx-API-Topic.htm>`_
+        
+        |
+Keywords: cloudshell,sandbox,api,CI
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Natural Language :: English
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
new file mode 100644
index 0000000..e9351d7
--- /dev/null
+++ b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
@@ -0,0 +1,11 @@
+README.rst
+setup.py
+cloudshell/__init__.py
+cloudshell/sandbox_rest/__init__.py
+cloudshell/sandbox_rest/flow_helpers.py
+cloudshell/sandbox_rest/sandbox_api.py
+cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
+cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
+cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
+cloudshell_sandboxapi_wrapper.egg-info/requires.txt
+cloudshell_sandboxapi_wrapper.egg-info/top_level.txt
\ No newline at end of file
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt b/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt
new file mode 100644
index 0000000..f229360
--- /dev/null
+++ b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt
@@ -0,0 +1 @@
+requests
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt b/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt
new file mode 100644
index 0000000..127ff4f
--- /dev/null
+++ b/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt
@@ -0,0 +1 @@
+cloudshell
diff --git a/dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz b/dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz
new file mode 100644
index 0000000000000000000000000000000000000000..6424e232d889ab652ddd54d37f98245b742d9a01
GIT binary patch
diff --git a/setup.py b/setup.py
index 919634e..a6d1d5e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,26 +1,35 @@
-from setuptools import setup
+from setuptools import setup, find_packages
+from typing import List
 
+def read_file(file_name: str) -> str:
+    with open(file_name) as f:
+        content = f.read().strip()
+    return content
 
-def readme():
-    with open('README.rst') as f:
-        return f.read()
+
+def lines_from_file(file_name: str) -> List[str]:
+    with open(file_name) as f:
+        lines = f.read().splitlines()
+    return lines
 
 
 setup(
     name='cloudshell_sandboxapi_wrapper',
-    version='1.0.0',
-    packages=['cloudshell_sandboxapi_wrapper'],
-    url='http://www.quali.com',
-    license='Apache 2.0',
+    description='Python client for CloudShell Sandbox REST api - consume sandboxes via CI',
+    keywords=["cloudshell", "sandbox", "api", "CI"],
+    url='https://github.com/QualiSystemsLab/Sandbox-API-Python',
     author='sadanand.s',
     author_email='sadanand.s@quali.com',
-    description='Python wrapper for CloudShell Sandbox API',
-    long_description=readme(),
+    license='Apache 2.0',
+    packages=find_packages(),
+    version=read_file("version.txt"),
+    long_description=read_file("README.rst"),
+    install_requires=lines_from_file("requirements.txt"),
+    test_requires=lines_from_file("test-requirements.txt"),
     classifiers=[
         'License :: OSI Approved :: Apache Software License',
         'Natural Language :: English',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',
-    ],
-    install_requires=['requests']
-)
+    ]
+)
\ No newline at end of file
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..55b033e
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1 @@
+pytest
\ No newline at end of file
diff --git a/tests/test_rest_client.py b/tests/test_rest_client.py
new file mode 100644
index 0000000..0e5af88
--- /dev/null
+++ b/tests/test_rest_client.py
@@ -0,0 +1,51 @@
+import pytest
+from cloudshell_test.sandbox_rest.sandbox_api import SandboxRestApiClient
+from datetime import datetime
+import json
+from dataclasses import dataclass
+
+DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template"
+DUT_RESOURCE = "DUT_1"
+HEALTH_CHECK_COMMAND = "health_check"
+
+
+@dataclass
+class SandboxTestData:
+    blueprint_name: str
+    sandbox_command: str
+
+
+@dataclass
+class ResourceTestData:
+    resource_name: str
+    resource_command: str
+
+
+@pytest.fixture(name="dut_bp")
+def dut_blueprint_fixture():
+    """ For testing basic operations """
+    return "DUT Test"
+
+
+@pytest.fixture(name="session")
+def rest_session_fixture():
+    return SandboxRestApiClient("localhost", "admin", "admin")
+
+
+def _pretty_print_response(dict_response):
+    json_str = json.dumps(dict_response, indent=4)
+    print(f"\n{json_str}")
+
+
+class TestNoBlueprint:
+    def test_get_sandboxes(self, session: SandboxRestApiClient):
+        res = session.get_sandboxes()
+        _pretty_print_response(res)
+
+
+class TestEmptyBlueprint:
+    bp_data = SandboxTestData(blueprint_name=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_command="Setup")
+
+    def test_simple_flow(self, session: SandboxRestApiClient):
+        start_response = session.start_sandbox(self.bp_data.blueprint_name, f"test sandbox - {datetime.now()}")
+        print(f"\nblueprint {start_response['name']} started")
diff --git a/version.txt b/version.txt
new file mode 100644
index 0000000..359a5b9
--- /dev/null
+++ b/version.txt
@@ -0,0 +1 @@
+2.0.0
\ No newline at end of file

From 25d1987b6b3d46a0898e70524c9a0c7cc285b437 Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Fri, 8 Oct 2021 23:42:00 +0100
Subject: [PATCH 10/47] components class added

---
 .../__pycache__/__init__.cpython-37.pyc       | Bin 0 -> 270 bytes
 .../__pycache__/exceptions.cpython-37.pyc     | Bin 802 -> 0 bytes
 .../__pycache__/sandbox_api.cpython-37.pyc    | Bin 0 -> 16768 bytes
 cloudshell/sandbox_rest/exceptions.py         |  13 ---
 .../{flow_helpers.py => helpers/__init__.py}  |   0
 .../helpers/concurrency_helper.py             |   0
 .../sandbox_rest/helpers/polling_helpers.py   |  96 +++++++++++++++++
 cloudshell/sandbox_rest/sandbox_api.py        |  35 +++++--
 cloudshell/sandbox_rest/sandbox_components.py |  98 ++++++++++++++++++
 cloudshell/sandbox_rest/sandbox_controller.py |  89 ++++++++++++++++
 requirements.txt                              |   3 +-
 11 files changed, 310 insertions(+), 24 deletions(-)
 create mode 100644 cloudshell/__pycache__/__init__.cpython-37.pyc
 delete mode 100644 cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc
 create mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc
 delete mode 100644 cloudshell/sandbox_rest/exceptions.py
 rename cloudshell/sandbox_rest/{flow_helpers.py => helpers/__init__.py} (100%)
 create mode 100644 cloudshell/sandbox_rest/helpers/concurrency_helper.py
 create mode 100644 cloudshell/sandbox_rest/helpers/polling_helpers.py
 create mode 100644 cloudshell/sandbox_rest/sandbox_components.py
 create mode 100644 cloudshell/sandbox_rest/sandbox_controller.py

diff --git a/cloudshell/__pycache__/__init__.cpython-37.pyc b/cloudshell/__pycache__/__init__.cpython-37.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1638dc996911c33a01b65615684ced948aabc95d
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/exceptions.cpython-37.pyc
deleted file mode 100644
index 178f54132054515b69782940d859da31f7c48a9c..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1c1a3bc81671dba32478e142b90ac16aedf5cf47
GIT binary patch
diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py
deleted file mode 100644
index 4b3a7ac..0000000
--- a/cloudshell/sandbox_rest/exceptions.py
+++ /dev/null
@@ -1,13 +0,0 @@
-class SandboxRestException(Exception):
-    """ General exception to raise inside Rest client class """
-    pass
-
-
-class SandboxRestAuthException(Exception):
-    """ Failed auth actions """
-    pass
-
-
-class SandboxRestInitException(ValueError):
-    """ Failed auth actions """
-    pass
diff --git a/cloudshell/sandbox_rest/flow_helpers.py b/cloudshell/sandbox_rest/helpers/__init__.py
similarity index 100%
rename from cloudshell/sandbox_rest/flow_helpers.py
rename to cloudshell/sandbox_rest/helpers/__init__.py
diff --git a/cloudshell/sandbox_rest/helpers/concurrency_helper.py b/cloudshell/sandbox_rest/helpers/concurrency_helper.py
new file mode 100644
index 0000000..e69de29
diff --git a/cloudshell/sandbox_rest/helpers/polling_helpers.py b/cloudshell/sandbox_rest/helpers/polling_helpers.py
new file mode 100644
index 0000000..d56bf59
--- /dev/null
+++ b/cloudshell/sandbox_rest/helpers/polling_helpers.py
@@ -0,0 +1,96 @@
+"""
+Helper functions to poll the state of sandbox setup or the execution status of a command.
+Using 'retrying' library to do polling:
+https://pypi.org/project/retrying/
+"""
+from typing import List, Callable
+from retrying import retry  # pip install retrying
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiClient
+from enum import Enum
+
+
+class SandboxStates(Enum):
+    before_setup_state = "BeforeSetup"
+    running_setup_state = "Setup"
+    error_state = "Error"
+    ready_state = "Ready"
+    teardown_state = "Teardown"
+    ended_state = "Ended"
+
+
+class ExecutionStatuses(Enum):
+    running_status = "Running"
+    pending_status = "Pending"
+    completed_status = "Completed"
+    failed_status = "Failed"
+
+
+UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value,
+                                 ExecutionStatuses.pending_status.value]
+
+
+def _should_we_keep_polling_setup(sandbox_details: dict) -> bool:
+    """ if still in setup keep polling """
+    setup_states = [SandboxStates.before_setup_state.value, SandboxStates.running_setup_state.value]
+    if sandbox_details["state"] in setup_states:
+        return True
+    return False
+
+
+def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool:
+    """ if in teardown keep polling """
+    if sandbox_details["state"] == SandboxStates.teardown_state.value:
+        return True
+    return False
+
+
+def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_func: Callable,
+                        max_polling_minutes: int, polling_frequency_seconds: int) -> str:
+    """ Create blocking polling process """
+
+    # retry wait times are in milliseconds
+    @retry(retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000,
+           stop_max_delay=max_polling_minutes * 60000)
+    def get_sandbox_details():
+        return api.get_sandbox_details(reservation_id)
+
+    return get_sandbox_details()
+
+
+def poll_sandbox_setup(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20,
+                       polling_frequency_seconds=30) -> dict:
+    """ poll until completion """
+    sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes,
+                                          polling_frequency_seconds)
+    return sandbox_details
+
+
+def poll_sandbox_teardown(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20,
+                          polling_frequency_seconds=30) -> dict:
+    """ poll until completion  """
+    sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes,
+                                          polling_frequency_seconds)
+    return sandbox_details
+
+
+def _should_we_keep_polling_execution(exc_data: dict) -> bool:
+    current_exc_status = exc_data["status"]
+    if current_exc_status in UNFINISHED_EXECUTION_STATUSES:
+        return True
+    return False
+
+
+def poll_execution_for_completion(sandbox_rest: SandboxRestApiClient, command_execution_id: str,
+                                  max_polling_in_minutes=20, polling_frequency_in_seconds=30) -> str:
+    """
+    poll execution for "Completed" status, then return the execution output
+    """
+    # retry wait times are in milliseconds
+    @retry(retry_on_result=_should_we_keep_polling_execution, wait_fixed=polling_frequency_in_seconds * 1000,
+           stop_max_delay=max_polling_in_minutes * 60000)
+    def get_execution_data():
+        exc_data = sandbox_rest.get_execution_details(command_execution_id)
+        return exc_data
+    return get_execution_data(sandbox_rest, command_execution_id)
+
+
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 3c99893..7b6803b 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -3,7 +3,21 @@
 from typing import List
 import requests
 from dataclasses import dataclass, asdict
-from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException
+
+
+class SandboxRestException(Exception):
+    """ General exception to raise inside Rest client class """
+    pass
+
+
+class SandboxRestAuthException(Exception):
+    """ Failed auth actions """
+    pass
+
+
+class SandboxRestInitException(ValueError):
+    """ Failed auth actions """
+    pass
 
 
 @dataclass
@@ -16,7 +30,7 @@ class InputParam:
     value: str
 
 
-class SandboxRestApiClient:
+class SandboxRestApiSession:
     """
     Python wrapper for CloudShell Sandbox API
     View http://<API_SERVER>/api/v2/explore to see schemas of return json values
@@ -207,7 +221,6 @@ def stop_sandbox(self, sandbox_id: str):
         response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'"))
-        return response.json()
 
     # SANDBOX GET REQUESTS
     def get_sandboxes(self, show_historic=False):
@@ -405,15 +418,17 @@ def delete_execution(self, execution_id: str):
     ADMIN_USER = "admin"
     ADMIN_PASS = "admin"
 
-    admin_api = SandboxRestApiClient(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS)
+    admin_api = SandboxRestApiSession(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS)
     with admin_api:
         # sample api call with admin user session to get all sandboxes
-        all_sandboxes = admin_api.get_sandboxes()
-        print("== List of sandboxes pulled by Admin ===")
-        print(json.dumps(all_sandboxes, indent=4))
+        # all_sandboxes = admin_api.get_sandboxes()
+        # print("== List of sandboxes pulled by Admin ===")
+        # print(json.dumps(all_sandboxes, indent=4))
+
+        sb_res = admin_api.start_sandbox(blueprint_id="rest test", sandbox_name="rest test")
+        print(sb_res["state"])
+        time.sleep(4)
+
 
-    admin_api.refresh_auth()
-    all_sandboxes = admin_api.get_sandboxes()
-    pass
 
 
diff --git a/cloudshell/sandbox_rest/sandbox_components.py b/cloudshell/sandbox_rest/sandbox_components.py
new file mode 100644
index 0000000..6fd9208
--- /dev/null
+++ b/cloudshell/sandbox_rest/sandbox_components.py
@@ -0,0 +1,98 @@
+import json
+from typing import List
+from enum import Enum
+
+
+class ComponentTypes(Enum):
+    app_type = "Application"
+    resource_type = "Resource"
+    service_type = "Service"
+
+
+class AppLifeCycleTypes(Enum):
+    deployed = "Deployed"
+    un_deployed = "Undeployed"
+
+
+class AttributeTypes(Enum):
+    boolean_type = "boolean"
+    password_type = "password"
+    string_type = "string"
+    numeric_type = "numeric"
+
+
+class SandboxRestComponents:
+    def __init__(self, components: List[dict] = None):
+        """
+        if instantiated with components then populate lists
+        if not, then call "refresh_components" after getting data from sandbox
+         """
+        self.all_components = components if components else []
+        self.services = []
+        self.resources = []
+        self.deployed_apps = []
+        self.un_deployed_apps = []
+        if self.all_components:
+            self._sort_components()
+
+    def _filter_components_by_type(self, component_type: str) -> List[dict]:
+        """ accepts both short info components and full info """
+        return [component for component in self.all_components if component["type"] == component_type]
+
+    def _filter_app_by_lifecycle(self, lifecycle_type):
+        return [component for component in self.all_components
+                if component["type"] == ComponentTypes.app_type.value
+                and component["app_lifecyle"] == lifecycle_type]
+
+    def _sort_components(self) -> None:
+        """ sort stored components into separate lists """
+        self.resources = self._filter_components_by_type(ComponentTypes.resource_type.value)
+        self.services = self._filter_components_by_type(ComponentTypes.service_type.value)
+        self.deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.deployed.value)
+        self.un_deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.un_deployed.value)
+
+    def refresh_components(self, components: List[dict]) -> None:
+        self.all_components = components
+        self._sort_components()
+
+    def find_component_by_name(self, component_name: str) -> dict:
+        for component in self.all_components:
+            if component["name"] == component_name:
+                return component
+
+    @staticmethod
+    def filter_by_model(components: List[dict], model: str) -> List[dict]:
+        """
+        Can pass in all components or sub list
+        Both Resources and Applications can use same shell / model
+        """
+        return [component for component in components if component["component_type"] == model]
+
+    def filter_by_attr_value(self, components: List[dict], attribute_name: str, attribute_value: str) -> List[dict]:
+        """ attribute name does not have to include model / family namespace """
+        self._validate_components_for_attributes(components)
+        result = []
+        for component in components:
+            for attr in component["attributes"]:
+                if attr["name"].endswith(attribute_name) and attr["value"] == attribute_value:
+                    result.append(component)
+        return result
+
+    def filter_by_boolean_attr_true(self, components: List[dict], attribute_name: str) -> List[dict]:
+        """ attribute name does not have to include model / family namespace """
+        self._validate_components_for_attributes(components)
+        result = []
+        for component in components:
+            for attr in component["attributes"]:
+                if attr["name"].endswith(attribute_name) and attr["value"].lower() == "true":
+                    result.append(component)
+        return result
+
+    @staticmethod
+    def _validate_components_for_attributes(components: List[dict]):
+        if not components:
+            return
+        attrs = components[0].get("attributes")
+        if not attrs:
+            raise Exception("'attributes' member not found. Must pass in Full info components.\n"
+                            f"components data passed:\n{json.dumps(components, indent=4)}")
diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py
new file mode 100644
index 0000000..1e52f33
--- /dev/null
+++ b/cloudshell/sandbox_rest/sandbox_controller.py
@@ -0,0 +1,89 @@
+import json
+import logging
+from typing import List
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam
+from cloudshell.sandbox_rest.helpers import polling_helpers as poll_help
+
+
+class SandboxSetupError(Exception):
+    """ When sandbox has error during setup """
+    pass
+
+
+class SandboxTeardownError(Exception):
+    """ When sandbox has error during setup """
+    pass
+
+
+class CommandFailedException(Exception):
+    pass
+
+
+# TODO:
+# add context manager methods
+# review design of class
+# add logger
+# publish env vars
+
+
+class SandboxRestController:
+    def __init__(self, api: SandboxRestApiSession, sandbox_id="", log_file_handler=False):
+        self.api = api
+        self.sandbox_id = sandbox_id
+        self.logger = None
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        pass
+
+    def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="PT1H30M",
+                               bp_params: List[InputParam] = None, permitted_users: List[str] = None,
+                               max_polling_minutes=30) -> dict:
+        """ Start sandbox, poll for result, get back sandbox info """
+        start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users)
+        sb_id = start_response["id"]
+        sandbox_details = poll_help.poll_sandbox_setup(self.api, sb_id, max_polling_minutes,
+                                                       polling_frequency_seconds=30)
+
+        sandbox_state = sandbox_details["state"]
+        stage = sandbox_details["setup_stage"]
+        name = sandbox_details["name"]
+
+        if "error" in sandbox_state.lower():
+            activity_errors = self._get_all_activity_errors(sb_id)
+
+            # TODO: print this? log it? or dump into exception message?
+            if activity_errors:
+                print(f"=== Activity Feed Errors ===\n{json.dumps(activity_errors, indent=4)}")
+
+            err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
+                       f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sb_id}'")
+            raise SandboxSetupError(err_msg)
+        return sandbox_details
+
+    def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30) -> dict:
+        self.api.stop_sandbox(sandbox_id)
+        sandbox_details = poll_help.poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes,
+                                                          polling_frequency_seconds=30)
+        sandbox_state = sandbox_details["state"]
+        stage = sandbox_details["setup_stage"]
+        name = sandbox_details["name"]
+
+        if "error" in sandbox_state.lower():
+            tail_error_count = 5
+            tailed_errors = self.api.get_sandbox_activity(sandbox_id, error_only=True, tail=tail_error_count)["events"]
+            print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}")
+            err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
+                       f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'")
+            raise SandboxTeardownError(err_msg)
+        return sandbox_details
+
+    def _get_all_activity_errors(self, sandbox_id):
+        activity_results = self.api.get_sandbox_activity(sandbox_id, error_only=True)
+        return activity_results["events"]
+
+    def publish_sandbox_id_to_env_vars(self):
+        pass
diff --git a/requirements.txt b/requirements.txt
index 663bd1f..e886ed4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
-requests
\ No newline at end of file
+requests
+retrying
\ No newline at end of file

From e7223620da68be2119ec264e275cd286054d18bd Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Sat, 9 Oct 2021 15:52:08 +0100
Subject: [PATCH 11/47] added state management to start / stop commands

---
 .../__pycache__/sandbox_api.cpython-37.pyc    | Bin 16768 -> 17230 bytes
 .../sandbox_components.cpython-37.pyc         | Bin 0 -> 5053 bytes
 .../__pycache__/__init__.cpython-37.pyc       | Bin 0 -> 191 bytes
 .../polling_helpers.cpython-37.pyc            | Bin 0 -> 4240 bytes
 .../helpers/concurrency_helper.py             |   0
 .../sandbox_rest/helpers/polling_helpers.py   |  40 ++++-
 cloudshell/sandbox_rest/sandbox_controller.py | 154 +++++++++++++-----
 7 files changed, 146 insertions(+), 48 deletions(-)
 create mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc
 create mode 100644 cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc
 create mode 100644 cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc
 delete mode 100644 cloudshell/sandbox_rest/helpers/concurrency_helper.py

diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc
index 1c1a3bc81671dba32478e142b90ac16aedf5cf47..9523de5f9bd52b6931ff603871c27a3ba6e0b6b9 100644
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1840cfc278748ee38bd9e3c24ab13b2c07d34784
GIT binary patch
diff --git a/cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2b20b3f99c96cfe3923b9169a2ea40bf89947b6b
GIT binary patch
diff --git a/cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc b/cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..18e0a2b4aa9cb546cee1bcb3c1a4cdae242d1c9f
GIT binary patch
diff --git a/cloudshell/sandbox_rest/helpers/concurrency_helper.py b/cloudshell/sandbox_rest/helpers/concurrency_helper.py
deleted file mode 100644
index e69de29..0000000
diff --git a/cloudshell/sandbox_rest/helpers/polling_helpers.py b/cloudshell/sandbox_rest/helpers/polling_helpers.py
index d56bf59..921bfc5 100644
--- a/cloudshell/sandbox_rest/helpers/polling_helpers.py
+++ b/cloudshell/sandbox_rest/helpers/polling_helpers.py
@@ -4,11 +4,19 @@
 https://pypi.org/project/retrying/
 """
 from typing import List, Callable
-from retrying import retry  # pip install retrying
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiClient
+from retrying import retry, RetryError  # pip install retrying
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 from enum import Enum
 
 
+class OrchestrationPollingTimeout(Exception):
+    pass
+
+
+class CommandPollingTimeout(Exception):
+    pass
+
+
 class SandboxStates(Enum):
     before_setup_state = "BeforeSetup"
     running_setup_state = "Setup"
@@ -25,6 +33,12 @@ class ExecutionStatuses(Enum):
     failed_status = "Failed"
 
 
+SANDBOX_SETUP_STATES = [SandboxStates.before_setup_state.value,
+                        SandboxStates.running_setup_state.value]
+
+SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value,
+                         SandboxStates.error_state.value]
+
 UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value,
                                  ExecutionStatuses.pending_status.value]
 
@@ -44,7 +58,7 @@ def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool:
     return False
 
 
-def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_func: Callable,
+def _poll_sandbox_state(api: SandboxRestApiSession, reservation_id: str, polling_func: Callable,
                         max_polling_minutes: int, polling_frequency_seconds: int) -> str:
     """ Create blocking polling process """
 
@@ -54,10 +68,14 @@ def _poll_sandbox_state(api: SandboxRestApiClient, reservation_id: str, polling_
     def get_sandbox_details():
         return api.get_sandbox_details(reservation_id)
 
-    return get_sandbox_details()
+    try:
+        sandbox_details = get_sandbox_details()
+    except RetryError:
+        raise OrchestrationPollingTimeout(f"Sandbox Polling timed out after configured {max_polling_minutes} minutes")
+    return sandbox_details
 
 
-def poll_sandbox_setup(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20,
+def poll_sandbox_setup(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20,
                        polling_frequency_seconds=30) -> dict:
     """ poll until completion """
     sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes,
@@ -65,7 +83,7 @@ def poll_sandbox_setup(api: SandboxRestApiClient, reservation_id: str, max_polli
     return sandbox_details
 
 
-def poll_sandbox_teardown(api: SandboxRestApiClient, reservation_id: str, max_polling_minutes=20,
+def poll_sandbox_teardown(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20,
                           polling_frequency_seconds=30) -> dict:
     """ poll until completion  """
     sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes,
@@ -80,17 +98,21 @@ def _should_we_keep_polling_execution(exc_data: dict) -> bool:
     return False
 
 
-def poll_execution_for_completion(sandbox_rest: SandboxRestApiClient, command_execution_id: str,
+def poll_execution_for_completion(sandbox_rest: SandboxRestApiSession, command_execution_id: str,
                                   max_polling_in_minutes=20, polling_frequency_in_seconds=30) -> str:
     """
     poll execution for "Completed" status, then return the execution output
     """
+
     # retry wait times are in milliseconds
     @retry(retry_on_result=_should_we_keep_polling_execution, wait_fixed=polling_frequency_in_seconds * 1000,
            stop_max_delay=max_polling_in_minutes * 60000)
     def get_execution_data():
         exc_data = sandbox_rest.get_execution_details(command_execution_id)
         return exc_data
-    return get_execution_data(sandbox_rest, command_execution_id)
-
 
+    try:
+        exc_data = get_execution_data(sandbox_rest, command_execution_id)
+    except RetryError:
+        raise CommandPollingTimeout(f"Execution polling timed out after max {max_polling_in_minutes} minutes")
+    return exc_data
diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py
index 1e52f33..93f4b64 100644
--- a/cloudshell/sandbox_rest/sandbox_controller.py
+++ b/cloudshell/sandbox_rest/sandbox_controller.py
@@ -1,9 +1,10 @@
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam
+from cloudshell.sandbox_rest.helpers.polling_helpers import poll_sandbox_setup, poll_sandbox_teardown, \
+    poll_execution_for_completion, SandboxStates, SANDBOX_SETUP_STATES, SANDBOX_ACTIVE_STATES
+from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents
 import json
-import logging
 from typing import List
-
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam
-from cloudshell.sandbox_rest.helpers import polling_helpers as poll_help
+from timeit import default_timer
 
 
 class SandboxSetupError(Exception):
@@ -20,70 +21,145 @@ class CommandFailedException(Exception):
     pass
 
 
-# TODO:
-# add context manager methods
-# review design of class
-# add logger
-# publish env vars
-
-
 class SandboxRestController:
-    def __init__(self, api: SandboxRestApiSession, sandbox_id="", log_file_handler=False):
+    def __init__(self, api: SandboxRestApiSession, sandbox_id="", disable_prints=False):
         self.api = api
         self.sandbox_id = sandbox_id
-        self.logger = None
+        self.components = SandboxRestComponents()
+        self.setup_finished = False
+        self.sandbox_ended = False
+        self.setup_errors: List[dict] = None
+        self.teardown_errors: List[dict] = None
+        self.disable_prints = disable_prints
+
+        # when passing in existing sandbox id update instance state, otherwise let "start sandbox" handle it
+        if self.sandbox_id:
+            self._handle_sandbox_id_on_init()
 
     def __enter__(self):
         return self
 
     def __exit__(self, exc_type, exc_val, exc_tb):
-        pass
+        if exc_type:
+            err_msg = f"Exiting sandbox scope with error. f{exc_type}: {exc_val}"
+            self._print(err_msg)
+
+    def _handle_sandbox_id_on_init(self):
+        self.update_components()
+        self.update_state_flags()
+
+    def update_components(self):
+        components = self.api.get_sandbox_components(self.sandbox_id)
+        self.components.refresh_components(components)
+
+    def update_state_flags(self):
+        details = self.api.get_sandbox_details(self.sandbox_id)
+        sandbox_state = details["state"]
+        if sandbox_state not in SANDBOX_SETUP_STATES:
+            self.setup_finished = True
+        elif sandbox_state not in SANDBOX_ACTIVE_STATES:
+            self.sandbox_ended = True
+
+    def _print(self, message):
+        if not self.disable_prints:
+            print(message)
 
     def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="PT1H30M",
                                bp_params: List[InputParam] = None, permitted_users: List[str] = None,
-                               max_polling_minutes=30) -> dict:
+                               max_polling_minutes=30, raise_setup_exception=True) -> dict:
         """ Start sandbox, poll for result, get back sandbox info """
-        start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users)
-        sb_id = start_response["id"]
-        sandbox_details = poll_help.poll_sandbox_setup(self.api, sb_id, max_polling_minutes,
-                                                       polling_frequency_seconds=30)
+        if self.sandbox_id:
+            raise ValueError(f"Sandbox already has id '{self.sandbox_id}'.\n"
+                             f"Start blueprint with new sandbox controller instance")
 
+        self._print(f"Starting blueprint {blueprint_id}")
+        start = default_timer()
+        start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users)
+        self.sandbox_id = start_response["id"]
+        sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes,
+                                             polling_frequency_seconds=30)
+        self.update_components()
         sandbox_state = sandbox_details["state"]
         stage = sandbox_details["setup_stage"]
         name = sandbox_details["name"]
 
-        if "error" in sandbox_state.lower():
-            activity_errors = self._get_all_activity_errors(sb_id)
+        if sandbox_state == SandboxStates.ready_state.value:
+            self.setup_finished = True
 
-            # TODO: print this? log it? or dump into exception message?
+        # scan activity feed for errors
+        if sandbox_state == SandboxStates.error_state.value:
+            self.setup_finished = True
+            activity_errors = self._get_all_activity_errors(self.sandbox_id)
             if activity_errors:
-                print(f"=== Activity Feed Errors ===\n{json.dumps(activity_errors, indent=4)}")
-
-            err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
-                       f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sb_id}'")
-            raise SandboxSetupError(err_msg)
+                # print and store setup error data
+                self.setup_errors = activity_errors
+                err_msg = f"Error Events during setup:\n{json.dumps(activity_errors, indent=4)}"
+                self._print(err_msg)
+
+            if raise_setup_exception:
+                err_msg = (f"Sandbox '{name}' Error during SETUP. See events for details.\n"
+                           f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'")
+                raise SandboxSetupError(err_msg)
+
+        total_minutes = (default_timer() - start) / 60
+        self._print(f"Setup finished after {total_minutes:.2f} minutes.")
         return sandbox_details
 
-    def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30) -> dict:
+    def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_teardown_exception=True) -> dict:
+        if self.sandbox_ended:
+            raise ValueError(f"sandbox {self.sandbox_id} already in completed state")
+
+        last_activity_id = self.api.get_sandbox_activity(self.sandbox_id, tail=1)["events"]["id"]
+        start = default_timer()
         self.api.stop_sandbox(sandbox_id)
-        sandbox_details = poll_help.poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes,
-                                                          polling_frequency_seconds=30)
+        sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes,
+                                                polling_frequency_seconds=30)
+        self.update_components()
         sandbox_state = sandbox_details["state"]
         stage = sandbox_details["setup_stage"]
         name = sandbox_details["name"]
 
-        if "error" in sandbox_state.lower():
-            tail_error_count = 5
-            tailed_errors = self.api.get_sandbox_activity(sandbox_id, error_only=True, tail=tail_error_count)["events"]
-            print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}")
-            err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
-                       f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'")
-            raise SandboxTeardownError(err_msg)
+        if sandbox_state == SandboxStates.ended_state.value:
+            self.sandbox_ended = True
+
+        tail_error_count = 5
+        tailed_errors = self.api.get_sandbox_activity(sandbox_id,
+                                                      error_only=True,
+                                                      from_event_id=last_activity_id + 1,
+                                                      tail=tail_error_count)["events"]
+        if tailed_errors:
+            self.sandbox_ended = True
+            self.teardown_errors = tailed_errors
+            self._print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}")
+
+            if raise_teardown_exception:
+                err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
+                           f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'")
+                raise SandboxTeardownError(err_msg)
+
+        total_minutes = (default_timer() - start) / 60
+        self._print(f"Teardown done in {total_minutes:.2f} minutes.")
         return sandbox_details
 
+    def rerun_setup(self):
+        pass
+
+    def run_sandbox_command_and_poll(self):
+        pass
+
+    def run_component_command_and_poll(self):
+        pass
+
     def _get_all_activity_errors(self, sandbox_id):
-        activity_results = self.api.get_sandbox_activity(sandbox_id, error_only=True)
-        return activity_results["events"]
+        return self.api.get_sandbox_activity(sandbox_id, error_only=True)["events"]
 
     def publish_sandbox_id_to_env_vars(self):
+        """ publish sandbox id as environment variable for different CI process to pick up """
         pass
+
+
+if __name__ == "__main__":
+    api = SandboxRestApiSession("localhost", "admin", "admin")
+    controller = SandboxRestController(api)
+    response = controller.start_sandbox_and_poll("rest test", "lolol")
+    print(json.dumps(response, indent=4))

From faa1d66ac0ce7f9a51f2c8e9418e3d8470f6b11b Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Fri, 22 Oct 2021 15:42:35 +0300
Subject: [PATCH 12/47] basic tests added

---
 .pre-commit-config.yaml                       |  44 ++++
 build/lib/cloudshell/__init__.py              |   5 +-
 build/lib/cloudshell/sandbox_rest/__init__.py |   5 +-
 .../cloudshell/sandbox_rest/sandbox_api.py    | 147 ++++++------
 cloudshell/__init__.py                        |   5 +-
 cloudshell/sandbox_rest/__init__.py           |   5 +-
 .../sandbox_rest/helpers/polling_helpers.py   |  69 +++---
 cloudshell/sandbox_rest/sandbox_api.py        | 221 +++++++++---------
 cloudshell/sandbox_rest/sandbox_components.py |  18 +-
 cloudshell/sandbox_rest/sandbox_controller.py |  63 +++--
 setup.py                                      |  28 +--
 tests/common.py                               |  10 +
 tests/common_fixtures.py                      |  20 ++
 tests/test_api _no_sandbox.py                 |  35 +++
 tests/test_api_with_sandbox.py                |  33 +++
 tests/test_rest_client.py                     |  51 ----
 tests/test_sandbox_controller.py              |   0
 17 files changed, 454 insertions(+), 305 deletions(-)
 create mode 100644 .pre-commit-config.yaml
 create mode 100644 tests/common.py
 create mode 100644 tests/common_fixtures.py
 create mode 100644 tests/test_api _no_sandbox.py
 create mode 100644 tests/test_api_with_sandbox.py
 delete mode 100644 tests/test_rest_client.py
 create mode 100644 tests/test_sandbox_controller.py

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..e3d0d87
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,44 @@
+repos:
+  - repo: https://github.com/timothycrosley/isort
+    rev: 5.4.2
+    hooks:
+      - id: isort
+        language_version: python3.7
+        args: [--line-length=127]
+  - repo: https://github.com/python/black
+    rev: 20.8b1
+    hooks:
+      - id: black
+        language_version: python3.7
+        args: [--line-length=127]
+  - repo: https://gitlab.com/pycqa/flake8
+    rev: 3.8.3
+    hooks:
+      - id: flake8
+        additional_dependencies: [
+          flake8-docstrings,
+          flake8-builtins,
+          flake8-comprehensions,
+          flake8-print,
+          flake8-eradicate,
+        ]
+        language_version: python3.7
+        args: [
+          --max-line-length=127,
+          '--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D200,D210,D401,W503,E203'
+        ]
+  # See https://stackoverflow.com/questions/61238318/pylint-and-pre-commit-hook-unable-to-import/61238571#61238571
+  - repo: local
+    hooks:
+      - id: pylint
+        name: pylint
+        entry: pylint
+        language: system
+        types: [python]
+        args: [
+          --max-line-length=127,
+          --max-public-methods=32,
+          --max-args=8,
+          '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding',
+          '--good-names=ip,rc,eval'
+        ]
\ No newline at end of file
diff --git a/build/lib/cloudshell/__init__.py b/build/lib/cloudshell/__init__.py
index b50dc4a..cee05d2 100644
--- a/build/lib/cloudshell/__init__.py
+++ b/build/lib/cloudshell/__init__.py
@@ -1,3 +1,4 @@
-__author__ = 'quali'
+__author__ = "quali"
 from pkgutil import extend_path
-__path__ = extend_path(__path__, __name__)
\ No newline at end of file
+
+__path__ = extend_path(__path__, __name__)
diff --git a/build/lib/cloudshell/sandbox_rest/__init__.py b/build/lib/cloudshell/sandbox_rest/__init__.py
index b50dc4a..cee05d2 100644
--- a/build/lib/cloudshell/sandbox_rest/__init__.py
+++ b/build/lib/cloudshell/sandbox_rest/__init__.py
@@ -1,3 +1,4 @@
-__author__ = 'quali'
+__author__ = "quali"
 from pkgutil import extend_path
-__path__ = extend_path(__path__, __name__)
\ No newline at end of file
+
+__path__ = extend_path(__path__, __name__)
diff --git a/build/lib/cloudshell/sandbox_rest/sandbox_api.py b/build/lib/cloudshell/sandbox_rest/sandbox_api.py
index ecc62ef..6b1bf2b 100644
--- a/build/lib/cloudshell/sandbox_rest/sandbox_api.py
+++ b/build/lib/cloudshell/sandbox_rest/sandbox_api.py
@@ -1,18 +1,20 @@
 import json
+from dataclasses import asdict, dataclass
 from typing import List
 
 import requests
-from dataclasses import dataclass, asdict
 
 
 class SandboxRestException(Exception):
     """ General exception to raise inside Rest client class """
+
     pass
 
 
 @dataclass
 class InputParam:
     """ To model the param objects passed to sandbox / component command endpoints """
+
     name: str
     value: str
 
@@ -23,8 +25,7 @@ class SandboxRestApiClient:
     View http://<API_SERVER>/api/v2/explore to see schemas of return json values
     """
 
-    def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False,
-                 api_version="v2"):
+    def __init__(self, host: str, username: str, password: str, domain="Global", port=82, is_https=False, api_version="v2"):
         """ login to api and store headers for future requests """
         protocol = "https" if is_https else "http"
         self._base_url = f"{protocol}://{host}:{port}/api"
@@ -35,19 +36,18 @@ def _get_auth_headers(self, user_name: str, password: str, domain: str):
         """
         Get token from login response, then place token into auth headers on class
         """
-        login_res = requests.put(url=f"{self._base_url}/login",
-                                 data=json.dumps({"username": user_name, "password": password, "domain": domain}),
-                                 headers={"Content-Type": "application/json"})
+        login_res = requests.put(
+            url=f"{self._base_url}/login",
+            data=json.dumps({"username": user_name, "password": password, "domain": domain}),
+            headers={"Content-Type": "application/json"},
+        )
 
         if not login_res.ok:
             raise SandboxRestException(self._format_err(login_res, "Failed Login"))
 
         login_token = login_res.text[1:-1]
 
-        auth_headers = {
-            'Authorization': f'Basic {login_token}',
-            'Content-Type': 'application/json'
-        }
+        auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"}
         return auth_headers
 
     @staticmethod
@@ -58,20 +58,26 @@ def _format_err(response: requests.Response, custom_err_msg="Failed Api Call"):
         return err_msg
 
     # SANDBOX POST REQUESTS
-    def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
-                      bp_params: List[InputParam] = None, permitted_users: List[str] = None):
+    def start_sandbox(
+        self,
+        blueprint_id: str,
+        sandbox_name="",
+        duration="PT2H0M",
+        bp_params: List[InputParam] = None,
+        permitted_users: List[str] = None,
+    ):
         """
         Create a sandbox from the provided blueprint id
         Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
         """
-        url = f'{self._versioned_url}/blueprints/{blueprint_id}/start'
-        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name']
+        url = f"{self._versioned_url}/blueprints/{blueprint_id}/start"
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
 
         data_dict = {
             "duration": duration,
             "name": sandbox_name,
             "permitted_users": permitted_users if permitted_users else [],
-            "params": [asdict(x) for x in bp_params] if bp_params else []
+            "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
         response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
@@ -80,16 +86,17 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
             raise SandboxRestException(err_msg)
         return response.json()
 
-    def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None,
-                                 permitted_users: List[str] = None):
+    def start_persistent_sandbox(
+        self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None
+    ):
         """ Create a persistent sandbox from the provided blueprint id """
-        url = f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent'
+        url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent"
 
-        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name']
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
         data_dict = {
             "name": sandbox_name,
             "permitted_users": permitted_users if permitted_users else [],
-            "params": [asdict(x) for x in bp_params] if bp_params else []
+            "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
         response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
@@ -98,10 +105,9 @@ def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params
             raise SandboxRestException(err_msg)
         return response.json()
 
-    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None,
-                            print_output=True):
+    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True):
         """ Run a sandbox level command """
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start"
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
@@ -110,17 +116,17 @@ def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[I
             raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'"))
         return response.json()
 
-    def run_component_command(self, sandbox_id: str, component_id: str, command_name: str,
-                              params: List[InputParam] = None, print_output: bool = True):
+    def run_component_command(
+        self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True
+    ):
         """ Start a command on sandbox component """
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start"
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
         response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                self._format_err(response, f"failed to start component command '{command_name}'"))
+            raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'"))
         return response.json()
 
     def extend_sandbox(self, sandbox_id: str, duration: str):
@@ -130,16 +136,16 @@ def extend_sandbox(self, sandbox_id: str, duration: str):
         :return:
         """
         data_dict = {"extended_time": duration}
-        response = requests.post(f'{self._base_url}/sandboxes/{sandbox_id}/extend',
-                                 data=json.dumps(data_dict),
-                                 headers=self._auth_headers)
+        response = requests.post(
+            f"{self._base_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers
+        )
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'"))
         return response.json()
 
     def stop_sandbox(self, sandbox_id: str):
         """ Stop the sandbox given sandbox id """
-        response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers)
+        response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'"))
         return response.json()
@@ -150,20 +156,19 @@ def get_sandboxes(self, show_historic=False):
         :return:
         """
         params = {"show_historic": show_historic}
-        response = requests.get(f'{self._versioned_url}/sandboxes', headers=self._auth_headers, params=params)
+        response = requests.get(f"{self._versioned_url}/sandboxes", headers=self._auth_headers, params=params)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"Failed to get sandbox list"))
         return response.json()
 
     def get_sandbox_details(self, sandbox_id: str):
         """ Get details of the given sandbox id """
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}', headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'"))
         return response.json()
 
-    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None,
-                             tail: int = None):
+    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None):
         """
         Get list of sandbox activity
         'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
@@ -171,7 +176,7 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from
         'tail' - how many of the last entries you want to pull
         'error_only' - to filter for error events only
         """
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/activity'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/activity"
         params = {}
 
         if error_only:
@@ -194,24 +199,26 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from
 
     def get_sandbox_commands(self, sandbox_id: str):
         """ Get list of sandbox commands """
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'"))
         return response.json()
 
     def get_sandbox_command_details(self, sandbox_id: str, command_name: str):
         """ Get details of specific sandbox command """
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}',
-                                headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers
+        )
         if not response.ok:
-            err_msg = self._format_err(response,
-                                       f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'")
+            err_msg = self._format_err(
+                response, f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'"
+            )
             raise SandboxRestException(err_msg)
         return response.json()
 
     def get_sandbox_components(self, sandbox_id: str):
         """ Get list of sandbox components """
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components', headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers)
         if not response.ok:
             err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'")
             raise SandboxRestException(err_msg)
@@ -219,22 +226,26 @@ def get_sandbox_components(self, sandbox_id: str):
 
     def get_sandbox_component_details(self, sandbox_id: str, component_id: str):
         """ Get details of components in sandbox """
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}',
-                                headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", headers=self._auth_headers
+        )
         if not response.ok:
-            custom_err_msg = (f"Failed to get sandbox component details for component: '{component_id}', "
-                              f"sandbox: '{sandbox_id}'")
+            custom_err_msg = (
+                f"Failed to get sandbox component details for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
+            )
             err_msg = self._format_err(response, custom_err_msg)
             raise SandboxRestException(err_msg)
         return response.json()
 
     def get_sandbox_component_commands(self, sandbox_id: str, component_id: str):
         """ Get list of commands for a particular component in sandbox """
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands',
-                                headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers
+        )
         if not response.ok:
-            custom_err_msg = (f"Failed to get sandbox component commands list for component: '{component_id}', "
-                              f"sandbox: '{sandbox_id}'")
+            custom_err_msg = (
+                f"Failed to get sandbox component commands list for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
+            )
             err_msg = self._format_err(response, custom_err_msg)
             raise SandboxRestException(err_msg)
         return response.json()
@@ -242,28 +253,28 @@ def get_sandbox_component_commands(self, sandbox_id: str, component_id: str):
     def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str):
         """ Get details of a command of sandbox component """
         response = requests.get(
-            f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}',
-            headers=self._auth_headers)
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}",
+            headers=self._auth_headers,
+        )
         if not response.ok:
-            custom_err_msg = (f"Failed to get sandbox component command details for component: '{component_id}', "
-                              f"sandbox: '{sandbox_id}'")
+            custom_err_msg = (
+                f"Failed to get sandbox component command details for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
+            )
             err_msg = self._format_err(response, custom_err_msg)
             raise SandboxRestException(err_msg)
         return response.json()
 
     def get_sandbox_instructions(self, sandbox_id: str):
         """ pull the instruction text of sandbox """
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions',
-                                headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers)
         if not response.ok:
-            err_msg = self._format_err(response,
-                                       f"Failed to get sandbox instructions for '{sandbox_id}'")
+            err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'")
             raise SandboxRestException(err_msg)
         return response.json()
 
     def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None):
         """ Get list of sandbox output """
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions"
         params = {}
         if tail:
             params["tail"] = tail
@@ -278,15 +289,14 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i
             response = requests.get(url, headers=self._auth_headers)
 
         if not response.ok:
-            err_msg = self._format_err(response,
-                                       f"Failed to get sandbox instructions for '{sandbox_id}'")
+            err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'")
             raise SandboxRestException(err_msg)
         return response.json()
 
     # BLUEPRINT GET REQUESTS
     def get_blueprints(self):
         """ Get list of blueprints """
-        response = requests.get(f'{self._versioned_url}/blueprints', headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, "Failed to get blueprints"))
         return response.json()
@@ -298,27 +308,24 @@ def get_blueprint_details(self, blueprint_id: str):
         """
         response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'")
         if not response.ok:
-            raise SandboxRestException(
-                self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'"))
+            raise SandboxRestException(self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'"))
         return response.json()
 
     # EXECUTIONS
     def get_execution_details(self, execution_id: str):
         response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                self._format_err(response, f"Failed to get execution details for '{execution_id}'"))
+            raise SandboxRestException(self._format_err(response, f"Failed to get execution details for '{execution_id}'"))
         return response.json()
 
     def delete_execution(self, execution_id: str):
         response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                self._format_err(response, f"Failed to delete execution for '{execution_id}'"))
+            raise SandboxRestException(self._format_err(response, f"Failed to delete execution for '{execution_id}'"))
         return response.json()
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     api = SandboxRestApiClient("localhost", "admin", "admin")
     bps = api.get_blueprints()
     pass
diff --git a/cloudshell/__init__.py b/cloudshell/__init__.py
index b50dc4a..cee05d2 100644
--- a/cloudshell/__init__.py
+++ b/cloudshell/__init__.py
@@ -1,3 +1,4 @@
-__author__ = 'quali'
+__author__ = "quali"
 from pkgutil import extend_path
-__path__ = extend_path(__path__, __name__)
\ No newline at end of file
+
+__path__ = extend_path(__path__, __name__)
diff --git a/cloudshell/sandbox_rest/__init__.py b/cloudshell/sandbox_rest/__init__.py
index b50dc4a..cee05d2 100644
--- a/cloudshell/sandbox_rest/__init__.py
+++ b/cloudshell/sandbox_rest/__init__.py
@@ -1,3 +1,4 @@
-__author__ = 'quali'
+__author__ = "quali"
 from pkgutil import extend_path
-__path__ = extend_path(__path__, __name__)
\ No newline at end of file
+
+__path__ = extend_path(__path__, __name__)
diff --git a/cloudshell/sandbox_rest/helpers/polling_helpers.py b/cloudshell/sandbox_rest/helpers/polling_helpers.py
index 921bfc5..531da57 100644
--- a/cloudshell/sandbox_rest/helpers/polling_helpers.py
+++ b/cloudshell/sandbox_rest/helpers/polling_helpers.py
@@ -3,10 +3,12 @@
 Using 'retrying' library to do polling:
 https://pypi.org/project/retrying/
 """
-from typing import List, Callable
-from retrying import retry, RetryError  # pip install retrying
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 from enum import Enum
+from typing import Callable, List
+
+from retrying import RetryError, retry  # pip install retrying
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 class OrchestrationPollingTimeout(Exception):
@@ -18,8 +20,8 @@ class CommandPollingTimeout(Exception):
 
 
 class SandboxStates(Enum):
-    before_setup_state = "BeforeSetup"
-    running_setup_state = "Setup"
+    BEFORE_SETUP_STATE = "BeforeSetup"
+    RUNNING_SETUP_STATE = "Setup"
     error_state = "Error"
     ready_state = "Ready"
     teardown_state = "Teardown"
@@ -33,19 +35,16 @@ class ExecutionStatuses(Enum):
     failed_status = "Failed"
 
 
-SANDBOX_SETUP_STATES = [SandboxStates.before_setup_state.value,
-                        SandboxStates.running_setup_state.value]
+SANDBOX_SETUP_STATES = [SandboxStates.BEFORE_SETUP_STATE.value, SandboxStates.RUNNING_SETUP_STATE.value]
 
-SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value,
-                         SandboxStates.error_state.value]
+SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value, SandboxStates.error_state.value]
 
-UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value,
-                                 ExecutionStatuses.pending_status.value]
+UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value, ExecutionStatuses.pending_status.value]
 
 
 def _should_we_keep_polling_setup(sandbox_details: dict) -> bool:
     """ if still in setup keep polling """
-    setup_states = [SandboxStates.before_setup_state.value, SandboxStates.running_setup_state.value]
+    setup_states = [SandboxStates.BEFORE_SETUP_STATE.value, SandboxStates.RUNNING_SETUP_STATE.value]
     if sandbox_details["state"] in setup_states:
         return True
     return False
@@ -58,13 +57,19 @@ def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool:
     return False
 
 
-def _poll_sandbox_state(api: SandboxRestApiSession, reservation_id: str, polling_func: Callable,
-                        max_polling_minutes: int, polling_frequency_seconds: int) -> str:
+def _poll_sandbox_state(
+    api: SandboxRestApiSession,
+    reservation_id: str,
+    polling_func: Callable,
+    max_polling_minutes: int,
+    polling_frequency_seconds: int,
+) -> str:
     """ Create blocking polling process """
 
     # retry wait times are in milliseconds
-    @retry(retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000,
-           stop_max_delay=max_polling_minutes * 60000)
+    @retry(
+        retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000, stop_max_delay=max_polling_minutes * 60000
+    )
     def get_sandbox_details():
         return api.get_sandbox_details(reservation_id)
 
@@ -75,19 +80,23 @@ def get_sandbox_details():
     return sandbox_details
 
 
-def poll_sandbox_setup(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20,
-                       polling_frequency_seconds=30) -> dict:
+def poll_sandbox_setup(
+    api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30
+) -> dict:
     """ poll until completion """
-    sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes,
-                                          polling_frequency_seconds)
+    sandbox_details = _poll_sandbox_state(
+        api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes, polling_frequency_seconds
+    )
     return sandbox_details
 
 
-def poll_sandbox_teardown(api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20,
-                          polling_frequency_seconds=30) -> dict:
+def poll_sandbox_teardown(
+    api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30
+) -> dict:
     """ poll until completion  """
-    sandbox_details = _poll_sandbox_state(api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes,
-                                          polling_frequency_seconds)
+    sandbox_details = _poll_sandbox_state(
+        api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes, polling_frequency_seconds
+    )
     return sandbox_details
 
 
@@ -98,15 +107,19 @@ def _should_we_keep_polling_execution(exc_data: dict) -> bool:
     return False
 
 
-def poll_execution_for_completion(sandbox_rest: SandboxRestApiSession, command_execution_id: str,
-                                  max_polling_in_minutes=20, polling_frequency_in_seconds=30) -> str:
+def poll_execution_for_completion(
+    sandbox_rest: SandboxRestApiSession, command_execution_id: str, max_polling_in_minutes=20, polling_frequency_in_seconds=30
+) -> str:
     """
     poll execution for "Completed" status, then return the execution output
     """
 
     # retry wait times are in milliseconds
-    @retry(retry_on_result=_should_we_keep_polling_execution, wait_fixed=polling_frequency_in_seconds * 1000,
-           stop_max_delay=max_polling_in_minutes * 60000)
+    @retry(
+        retry_on_result=_should_we_keep_polling_execution,
+        wait_fixed=polling_frequency_in_seconds * 1000,
+        stop_max_delay=max_polling_in_minutes * 60000,
+    )
     def get_execution_data():
         exc_data = sandbox_rest.get_execution_details(command_execution_id)
         return exc_data
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 7b6803b..ae4f970 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -1,22 +1,26 @@
 import json
 import time
+from dataclasses import asdict, dataclass
 from typing import List
+
 import requests
-from dataclasses import dataclass, asdict
 
 
 class SandboxRestException(Exception):
     """ General exception to raise inside Rest client class """
+
     pass
 
 
 class SandboxRestAuthException(Exception):
     """ Failed auth actions """
+
     pass
 
 
 class SandboxRestInitException(ValueError):
     """ Failed auth actions """
+
     pass
 
 
@@ -26,6 +30,7 @@ class InputParam:
     param objects passed to sandbox / component command endpoints
     sandbox global inputs, commands and resource commands all follow this generic name/value convention
     """
+
     name: str
     value: str
 
@@ -36,17 +41,21 @@ class SandboxRestApiSession:
     View http://<API_SERVER>/api/v2/explore to see schemas of return json values
     """
 
-    def __init__(self, host: str, username: str, password: str, domain='Global', port=82, is_https=False,
-                 api_version="v2"):
+    def __init__(
+        self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, api_version="v2"
+    ):
         """ login to api and store headers for future requests """
         _protocol = "https" if is_https else "http"
+        self._base_url = f"{_protocol}://{host}:{port}/api"
+        self._versioned_url = f"{self._base_url}/{api_version}"
         self.host = host
         self.username = username
         self._password = password
         self.domain = domain
-        self._base_url = f"{_protocol}://{host}:{port}/api"
-        self._versioned_url = f"{self._base_url}/{api_version}"
-        self.auth_token = self.get_token_with_credentials(username, password, domain)
+        if self.username and self._password:
+            self.auth_token = self.get_token_with_credentials(username, password, domain)
+        elif token:
+            self.auth_token = token
         self._auth_headers = self._build_auth_headers(self.auth_token)
 
     # CONTEXT MANAGER METHODS
@@ -62,18 +71,15 @@ def _build_auth_headers(login_token: str) -> dict:
         """
         interpolate token into auth_headers dict
         """
-        auth_headers = {
-            'Authorization': f'Basic {login_token}',
-            'Content-Type': 'application/json'
-        }
+        auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"}
         return auth_headers
 
-    def refresh_auth(self):
+    def refresh_auth_from_stored_credentials(self):
         self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain)
         self._auth_headers = self._build_auth_headers(self.auth_token)
 
     def invalidate_auth(self):
-        self.invalidate_target_token(self.auth_token)
+        self.delete_token(self.auth_token)
         self._auth_headers = None
 
     def _validate_auth_headers(self):
@@ -83,11 +89,13 @@ def _validate_auth_headers(self):
     # LOGIN METHODS
     def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str:
         """
-       Get token from credentials - extraneous quotes stripped off token string
-       """
-        login_res = requests.put(url=f"{self._base_url}/login",
-                                 data=json.dumps({"username": user_name, "password": password, "domain": domain}),
-                                 headers={"Content-Type": "application/json"})
+        Get token from credentials - extraneous quotes stripped off token string
+        """
+        login_res = requests.put(
+            url=f"{self._base_url}/login",
+            data=json.dumps({"username": user_name, "password": password, "domain": domain}),
+            headers={"Content-Type": "application/json"},
+        )
 
         if not login_res.ok:
             raise SandboxRestAuthException(self._format_err(login_res, "Failed Login"))
@@ -100,9 +108,9 @@ def get_token_for_target_user(self, user_name: str) -> str:
         Get token for target user - remove extraneous quotes
         """
         self._validate_auth_headers()
-        login_res = requests.post(url=f"{self._base_url}/token",
-                                  data=json.dumps({"username": user_name}),
-                                  headers=self._auth_headers)
+        login_res = requests.post(
+            url=f"{self._base_url}/token", data=json.dumps({"username": user_name}), headers=self._auth_headers
+        )
 
         if not login_res.ok:
             raise SandboxRestException(self._format_err(login_res, f"Failed to get get token for user {user_name}"))
@@ -110,37 +118,42 @@ def get_token_for_target_user(self, user_name: str) -> str:
         login_token = login_res.text[1:-1]
         return login_token
 
-    def invalidate_target_token(self, token_id: str) -> None:
+    def delete_token(self, token_id: str) -> None:
         self._validate_auth_headers()
-        login_res = requests.delete(url=f"{self._base_url}/token/{token_id}",
-                                    headers=self._auth_headers)
+        login_res = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers)
         if not login_res.ok:
             raise SandboxRestException(self._format_err(login_res, "Failed to delete token"))
 
     def _format_err(self, response: requests.Response, custom_err_msg="Failed Api Call"):
-        response_data = (f"Response: {response.status_code}, Reason: {response.reason}\n"
-                         f"Request URL: {response.request.url}\n"
-                         f"Request Headers: {response.request.headers}")
-        return (f"Sandbox API Error for User: '{self.username}', Domain: '{self.domain}'.\n"
-                f"{custom_err_msg}\n"
-                f"{response_data}")
+        response_data = (
+            f"Response: {response.status_code}, Reason: {response.reason}\n"
+            f"Request URL: {response.request.url}\n"
+            f"Request Headers: {response.request.headers}"
+        )
+        return f"Sandbox API Error for User: '{self.username}'.\n" f"{custom_err_msg}\n" f"{response_data}"
 
     # SANDBOX POST REQUESTS
-    def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
-                      bp_params: List[InputParam] = None, permitted_users: List[str] = None):
+    def start_sandbox(
+        self,
+        blueprint_id: str,
+        sandbox_name="",
+        duration="PT2H0M",
+        bp_params: List[InputParam] = None,
+        permitted_users: List[str] = None,
+    ) -> dict:
         """
         Create a sandbox from the provided blueprint id
         Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
         """
         self._validate_auth_headers()
-        url = f'{self._versioned_url}/blueprints/{blueprint_id}/start'
-        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name']
+        url = f"{self._versioned_url}/blueprints/{blueprint_id}/start"
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
 
         data_dict = {
             "duration": duration,
             "name": sandbox_name,
             "permitted_users": permitted_users if permitted_users else [],
-            "params": [asdict(x) for x in bp_params] if bp_params else []
+            "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
         response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
@@ -154,17 +167,18 @@ def start_sandbox(self, blueprint_id: str, sandbox_name="", duration="PT2H0M",
             raise SandboxRestException(err_msg)
         return response.json()
 
-    def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None,
-                                 permitted_users: List[str] = None):
+    def start_persistent_sandbox(
+        self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None
+    ):
         """ Create a persistent sandbox from the provided blueprint id """
         self._validate_auth_headers()
-        url = f'{self._versioned_url}/blueprints/{blueprint_id}/start-persistent'
+        url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent"
 
-        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)['name']
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
         data_dict = {
             "name": sandbox_name,
             "permitted_users": permitted_users if permitted_users else [],
-            "params": [asdict(x) for x in bp_params] if bp_params else []
+            "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
         response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
@@ -173,11 +187,10 @@ def start_persistent_sandbox(self, blueprint_id: str, sandbox_name="", bp_params
             raise SandboxRestException(err_msg)
         return response.json()
 
-    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None,
-                            print_output=True):
+    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True):
         """ Run a sandbox level command """
         self._validate_auth_headers()
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start"
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
@@ -186,18 +199,18 @@ def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[I
             raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'"))
         return response.json()
 
-    def run_component_command(self, sandbox_id: str, component_id: str, command_name: str,
-                              params: List[InputParam] = None, print_output: bool = True):
+    def run_component_command(
+        self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True
+    ):
         """ Start a command on sandbox component """
         self._validate_auth_headers()
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start"
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
         response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                self._format_err(response, f"failed to start component command '{command_name}'"))
+            raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'"))
         return response.json()
 
     def extend_sandbox(self, sandbox_id: str, duration: str):
@@ -208,9 +221,9 @@ def extend_sandbox(self, sandbox_id: str, duration: str):
         """
         self._validate_auth_headers()
         data_dict = {"extended_time": duration}
-        response = requests.post(f'{self._base_url}/sandboxes/{sandbox_id}/extend',
-                                 data=json.dumps(data_dict),
-                                 headers=self._auth_headers)
+        response = requests.post(
+            f"{self._base_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers
+        )
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'"))
         return response.json()
@@ -218,7 +231,7 @@ def extend_sandbox(self, sandbox_id: str, duration: str):
     def stop_sandbox(self, sandbox_id: str):
         """ Stop the sandbox given sandbox id """
         self._validate_auth_headers()
-        response = requests.post(f'{self._versioned_url}/sandboxes/{sandbox_id}/stop', headers=self._auth_headers)
+        response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'"))
 
@@ -226,26 +239,25 @@ def stop_sandbox(self, sandbox_id: str):
     def get_sandboxes(self, show_historic=False):
         """ Get list of sandboxes """
         self._validate_auth_headers()
-        url = f'{self._versioned_url}/sandboxes'
+        url = f"{self._versioned_url}/sandboxes"
         params = {"show_historic": "true" if show_historic else "false"}
         response = requests.get(url, headers=self._auth_headers, params=params)
         if not response.ok:
-            err_msg = self._format_err(response, f"Failed to get sandbox list")
+            err_msg = self._format_err(response, "Failed to get sandbox list")
             raise SandboxRestException(err_msg)
         return response.json()
 
     def get_sandbox_details(self, sandbox_id: str):
         """ Get details of the given sandbox id """
         self._validate_auth_headers()
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}"
         response = requests.get(url, headers=self._auth_headers)
         if not response.ok:
             exc_msg = self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'")
             raise SandboxRestException(exc_msg)
         return response.json()
 
-    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None,
-                             tail: int = None):
+    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None):
         """
         Get list of sandbox activity
         'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
@@ -254,7 +266,7 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from
         'error_only' - to filter for error events only
         """
         self._validate_auth_headers()
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/activity'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/activity"
         params = {}
 
         if error_only:
@@ -278,7 +290,7 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from
     def get_sandbox_commands(self, sandbox_id: str):
         """ Get list of sandbox commands """
         self._validate_auth_headers()
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands', headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'"))
         return response.json()
@@ -286,18 +298,20 @@ def get_sandbox_commands(self, sandbox_id: str):
     def get_sandbox_command_details(self, sandbox_id: str, command_name: str):
         """ Get details of specific sandbox command """
         self._validate_auth_headers()
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}',
-                                headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers
+        )
         if not response.ok:
-            err_msg = self._format_err(response,
-                                       f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'")
+            err_msg = self._format_err(
+                response, f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'"
+            )
             raise SandboxRestException(err_msg)
         return response.json()
 
     def get_sandbox_components(self, sandbox_id: str):
         """ Get list of sandbox components """
         self._validate_auth_headers()
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components', headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers)
         if not response.ok:
             err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'")
             raise SandboxRestException(err_msg)
@@ -306,11 +320,13 @@ def get_sandbox_components(self, sandbox_id: str):
     def get_sandbox_component_details(self, sandbox_id: str, component_id: str):
         """ Get details of components in sandbox """
         self._validate_auth_headers()
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}',
-                                headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", headers=self._auth_headers
+        )
         if not response.ok:
-            custom_err_msg = (f"Failed to get sandbox component details for component: '{component_id}', "
-                              f"sandbox: '{sandbox_id}'")
+            custom_err_msg = (
+                f"Failed to get sandbox component details for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
+            )
             err_msg = self._format_err(response, custom_err_msg)
             raise SandboxRestException(err_msg)
         return response.json()
@@ -318,11 +334,13 @@ def get_sandbox_component_details(self, sandbox_id: str, component_id: str):
     def get_sandbox_component_commands(self, sandbox_id: str, component_id: str):
         """ Get list of commands for a particular component in sandbox """
         self._validate_auth_headers()
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands',
-                                headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers
+        )
         if not response.ok:
-            custom_err_msg = (f"Failed to get sandbox component commands list for component: '{component_id}', "
-                              f"sandbox: '{sandbox_id}'")
+            custom_err_msg = (
+                f"Failed to get sandbox component commands list for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
+            )
             err_msg = self._format_err(response, custom_err_msg)
             raise SandboxRestException(err_msg)
         return response.json()
@@ -331,11 +349,13 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s
         """ Get details of a command of sandbox component """
         self._validate_auth_headers()
         response = requests.get(
-            f'{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}',
-            headers=self._auth_headers)
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}",
+            headers=self._auth_headers,
+        )
         if not response.ok:
-            custom_err_msg = (f"Failed to get sandbox component command details for component: '{component_id}', "
-                              f"sandbox: '{sandbox_id}'")
+            custom_err_msg = (
+                f"Failed to get sandbox component command details for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
+            )
             err_msg = self._format_err(response, custom_err_msg)
             raise SandboxRestException(err_msg)
         return response.json()
@@ -343,18 +363,16 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s
     def get_sandbox_instructions(self, sandbox_id: str):
         """ pull the instruction text of sandbox """
         self._validate_auth_headers()
-        response = requests.get(f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions',
-                                headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers)
         if not response.ok:
-            err_msg = self._format_err(response,
-                                       f"Failed to get sandbox instructions for '{sandbox_id}'")
+            err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'")
             raise SandboxRestException(err_msg)
         return response.json()
 
     def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None):
         """ Get list of sandbox output """
         self._validate_auth_headers()
-        url = f'{self._versioned_url}/sandboxes/{sandbox_id}/instructions'
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions"
         params = {}
         if tail:
             params["tail"] = tail
@@ -369,8 +387,7 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i
             response = requests.get(url, headers=self._auth_headers)
 
         if not response.ok:
-            err_msg = self._format_err(response,
-                                       f"Failed to get sandbox instructions for '{sandbox_id}'")
+            err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'")
             raise SandboxRestException(err_msg)
         return response.json()
 
@@ -378,7 +395,7 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i
     def get_blueprints(self):
         """ Get list of blueprints """
         self._validate_auth_headers()
-        response = requests.get(f'{self._versioned_url}/blueprints', headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(self._format_err(response, "Failed to get blueprints"))
         return response.json()
@@ -389,10 +406,9 @@ def get_blueprint_details(self, blueprint_id: str):
         Can pass either blueprint name OR blueprint ID
         """
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'", headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/blueprints/{blueprint_id}", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'"))
+            raise SandboxRestException(self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'"))
         return response.json()
 
     # EXECUTIONS
@@ -400,35 +416,28 @@ def get_execution_details(self, execution_id: str):
         self._validate_auth_headers()
         response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                self._format_err(response, f"Failed to get execution details for '{execution_id}'"))
+            raise SandboxRestException(self._format_err(response, f"Failed to get execution details for '{execution_id}'"))
         return response.json()
 
     def delete_execution(self, execution_id: str):
         self._validate_auth_headers()
         response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                self._format_err(response, f"Failed to delete execution for '{execution_id}'"))
+            raise SandboxRestException(self._format_err(response, f"Failed to delete execution for '{execution_id}'"))
         return response.json()
 
 
-if __name__ == '__main__':
-    API_SERVER = "localhost"
-    ADMIN_USER = "admin"
-    ADMIN_PASS = "admin"
-
-    admin_api = SandboxRestApiSession(host=API_SERVER, username=ADMIN_USER, password=ADMIN_PASS)
-    with admin_api:
-        # sample api call with admin user session to get all sandboxes
-        # all_sandboxes = admin_api.get_sandboxes()
-        # print("== List of sandboxes pulled by Admin ===")
-        # print(json.dumps(all_sandboxes, indent=4))
-
-        sb_res = admin_api.start_sandbox(blueprint_id="rest test", sandbox_name="rest test")
-        print(sb_res["state"])
-        time.sleep(4)
-
+if __name__ == "__main__":
+    TARGET_END_USER = "domain_admin"
+    TARGET_END_USER = "natti"
 
+    admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin")
 
+    with admin_api:
+        all_sandboxes = admin_api.get_sandboxes()
+        print(f"Sandbox count pulled by system admin: {len(all_sandboxes)}")
 
+        end_user_token = admin_api.get_token_for_target_user(TARGET_END_USER)
+        with SandboxRestApiSession(host="localhost", username=TARGET_END_USER, token=end_user_token) as user_rest:
+            user_bps = user_rest.get_blueprints()
+            print(f"Blueprint count pulled by user '{TARGET_END_USER}': {len(user_bps)}")
diff --git a/cloudshell/sandbox_rest/sandbox_components.py b/cloudshell/sandbox_rest/sandbox_components.py
index 6fd9208..7325eab 100644
--- a/cloudshell/sandbox_rest/sandbox_components.py
+++ b/cloudshell/sandbox_rest/sandbox_components.py
@@ -1,6 +1,6 @@
 import json
-from typing import List
 from enum import Enum
+from typing import List
 
 
 class ComponentTypes(Enum):
@@ -26,7 +26,7 @@ def __init__(self, components: List[dict] = None):
         """
         if instantiated with components then populate lists
         if not, then call "refresh_components" after getting data from sandbox
-         """
+        """
         self.all_components = components if components else []
         self.services = []
         self.resources = []
@@ -40,9 +40,11 @@ def _filter_components_by_type(self, component_type: str) -> List[dict]:
         return [component for component in self.all_components if component["type"] == component_type]
 
     def _filter_app_by_lifecycle(self, lifecycle_type):
-        return [component for component in self.all_components
-                if component["type"] == ComponentTypes.app_type.value
-                and component["app_lifecyle"] == lifecycle_type]
+        return [
+            component
+            for component in self.all_components
+            if component["type"] == ComponentTypes.app_type.value and component["app_lifecyle"] == lifecycle_type
+        ]
 
     def _sort_components(self) -> None:
         """ sort stored components into separate lists """
@@ -94,5 +96,7 @@ def _validate_components_for_attributes(components: List[dict]):
             return
         attrs = components[0].get("attributes")
         if not attrs:
-            raise Exception("'attributes' member not found. Must pass in Full info components.\n"
-                            f"components data passed:\n{json.dumps(components, indent=4)}")
+            raise Exception(
+                "'attributes' member not found. Must pass in Full info components.\n"
+                f"components data passed:\n{json.dumps(components, indent=4)}"
+            )
diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py
index 93f4b64..473c7c5 100644
--- a/cloudshell/sandbox_rest/sandbox_controller.py
+++ b/cloudshell/sandbox_rest/sandbox_controller.py
@@ -1,19 +1,28 @@
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession, InputParam
-from cloudshell.sandbox_rest.helpers.polling_helpers import poll_sandbox_setup, poll_sandbox_teardown, \
-    poll_execution_for_completion, SandboxStates, SANDBOX_SETUP_STATES, SANDBOX_ACTIVE_STATES
-from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents
 import json
-from typing import List
 from timeit import default_timer
+from typing import List
+
+from cloudshell.sandbox_rest.helpers.polling_helpers import (
+    SANDBOX_ACTIVE_STATES,
+    SANDBOX_SETUP_STATES,
+    SandboxStates,
+    poll_execution_for_completion,
+    poll_sandbox_setup,
+    poll_sandbox_teardown,
+)
+from cloudshell.sandbox_rest.sandbox_api import InputParam, SandboxRestApiSession
+from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents
 
 
 class SandboxSetupError(Exception):
     """ When sandbox has error during setup """
+
     pass
 
 
 class SandboxTeardownError(Exception):
     """ When sandbox has error during setup """
+
     pass
 
 
@@ -64,20 +73,27 @@ def _print(self, message):
         if not self.disable_prints:
             print(message)
 
-    def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="PT1H30M",
-                               bp_params: List[InputParam] = None, permitted_users: List[str] = None,
-                               max_polling_minutes=30, raise_setup_exception=True) -> dict:
+    def start_sandbox_and_poll(
+        self,
+        blueprint_id: str,
+        sandbox_name="",
+        duration="PT1H30M",
+        bp_params: List[InputParam] = None,
+        permitted_users: List[str] = None,
+        max_polling_minutes=30,
+        raise_setup_exception=True,
+    ) -> dict:
         """ Start sandbox, poll for result, get back sandbox info """
         if self.sandbox_id:
-            raise ValueError(f"Sandbox already has id '{self.sandbox_id}'.\n"
-                             f"Start blueprint with new sandbox controller instance")
+            raise ValueError(
+                f"Sandbox already has id '{self.sandbox_id}'.\n" f"Start blueprint with new sandbox controller instance"
+            )
 
         self._print(f"Starting blueprint {blueprint_id}")
         start = default_timer()
         start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users)
         self.sandbox_id = start_response["id"]
-        sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes,
-                                             polling_frequency_seconds=30)
+        sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes, polling_frequency_seconds=30)
         self.update_components()
         sandbox_state = sandbox_details["state"]
         stage = sandbox_details["setup_stage"]
@@ -97,8 +113,10 @@ def start_sandbox_and_poll(self, blueprint_id: str, sandbox_name="", duration="P
                 self._print(err_msg)
 
             if raise_setup_exception:
-                err_msg = (f"Sandbox '{name}' Error during SETUP. See events for details.\n"
-                           f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'")
+                err_msg = (
+                    f"Sandbox '{name}' Error during SETUP. See events for details.\n"
+                    f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'"
+                )
                 raise SandboxSetupError(err_msg)
 
         total_minutes = (default_timer() - start) / 60
@@ -112,8 +130,7 @@ def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_t
         last_activity_id = self.api.get_sandbox_activity(self.sandbox_id, tail=1)["events"]["id"]
         start = default_timer()
         self.api.stop_sandbox(sandbox_id)
-        sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes,
-                                                polling_frequency_seconds=30)
+        sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes, polling_frequency_seconds=30)
         self.update_components()
         sandbox_state = sandbox_details["state"]
         stage = sandbox_details["setup_stage"]
@@ -123,18 +140,19 @@ def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_t
             self.sandbox_ended = True
 
         tail_error_count = 5
-        tailed_errors = self.api.get_sandbox_activity(sandbox_id,
-                                                      error_only=True,
-                                                      from_event_id=last_activity_id + 1,
-                                                      tail=tail_error_count)["events"]
+        tailed_errors = self.api.get_sandbox_activity(
+            sandbox_id, error_only=True, from_event_id=last_activity_id + 1, tail=tail_error_count
+        )["events"]
         if tailed_errors:
             self.sandbox_ended = True
             self.teardown_errors = tailed_errors
             self._print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}")
 
             if raise_teardown_exception:
-                err_msg = (f"Sandbox '{name}' Error during SETUP.\n"
-                           f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'")
+                err_msg = (
+                    f"Sandbox '{name}' Error during SETUP.\n"
+                    f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'"
+                )
                 raise SandboxTeardownError(err_msg)
 
         total_minutes = (default_timer() - start) / 60
@@ -142,6 +160,7 @@ def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_t
         return sandbox_details
 
     def rerun_setup(self):
+        self
         pass
 
     def run_sandbox_command_and_poll(self):
diff --git a/setup.py b/setup.py
index a6d1d5e..cccdeec 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,8 @@
-from setuptools import setup, find_packages
 from typing import List
 
+from setuptools import find_packages, setup
+
+
 def read_file(file_name: str) -> str:
     with open(file_name) as f:
         content = f.read().strip()
@@ -14,22 +16,22 @@ def lines_from_file(file_name: str) -> List[str]:
 
 
 setup(
-    name='cloudshell_sandboxapi_wrapper',
-    description='Python client for CloudShell Sandbox REST api - consume sandboxes via CI',
+    name="cloudshell_sandboxapi_wrapper",
+    description="Python client for CloudShell Sandbox REST api - consume sandboxes via CI",
     keywords=["cloudshell", "sandbox", "api", "CI"],
-    url='https://github.com/QualiSystemsLab/Sandbox-API-Python',
-    author='sadanand.s',
-    author_email='sadanand.s@quali.com',
-    license='Apache 2.0',
+    url="https://github.com/QualiSystemsLab/Sandbox-API-Python",
+    author="sadanand.s",
+    author_email="sadanand.s@quali.com",
+    license="Apache 2.0",
     packages=find_packages(),
     version=read_file("version.txt"),
     long_description=read_file("README.rst"),
     install_requires=lines_from_file("requirements.txt"),
     test_requires=lines_from_file("test-requirements.txt"),
     classifiers=[
-        'License :: OSI Approved :: Apache Software License',
-        'Natural Language :: English',
-        'Programming Language :: Python :: 3.7',
-        'Programming Language :: Python :: 3.8',
-    ]
-)
\ No newline at end of file
+        "License :: OSI Approved :: Apache Software License",
+        "Natural Language :: English",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+    ],
+)
diff --git a/tests/common.py b/tests/common.py
new file mode 100644
index 0000000..8539c15
--- /dev/null
+++ b/tests/common.py
@@ -0,0 +1,10 @@
+import json
+
+DUT_RESOURCE = "DUT_1"
+DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template"
+HEALTH_CHECK_COMMAND = "health_check"
+
+
+def pretty_print_response(dict_response):
+    json_str = json.dumps(dict_response, indent=4)
+    print(f"\n{json_str}")
diff --git a/tests/common_fixtures.py b/tests/common_fixtures.py
new file mode 100644
index 0000000..5dcb85f
--- /dev/null
+++ b/tests/common_fixtures.py
@@ -0,0 +1,20 @@
+import pytest
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+
+
+@pytest.fixture
+def dut_bp_name() -> str:
+    """ For testing basic operations """
+    return "DUT Test"
+
+
+@pytest.fixture
+def dut_resource_name() -> str:
+    """ For testing basic operations """
+    return "DUT_1"
+
+
+@pytest.fixture
+def admin_session() -> SandboxRestApiSession:
+    return SandboxRestApiSession("localhost", "admin", "admin")
diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py
new file mode 100644
index 0000000..6ea9528
--- /dev/null
+++ b/tests/test_api _no_sandbox.py	
@@ -0,0 +1,35 @@
+"""
+Test the api methods that do not require a live sandbox
+- get all blueprints
+- get all sandboxes
+- get blueprint by id
+- get token + delete token
+"""
+from common import DEFAULT_BLUEPRINT_TEMPLATE, pretty_print_response
+from common_fixtures import admin_session
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+
+
+def test_get_sandboxes(admin_session: SandboxRestApiSession):
+    res = admin_session.get_sandboxes()
+    print(f"Sandbox count found in system: {len(res)}")
+
+
+def test_get_blueprints(admin_session: SandboxRestApiSession):
+    bp_res = admin_session.get_blueprints()
+    print(f"Blueprint count found in system: '{len(bp_res)}'")
+
+
+def test_get_default_blueprint(admin_session: SandboxRestApiSession):
+    bp_res = admin_session.get_blueprint_details(DEFAULT_BLUEPRINT_TEMPLATE)
+    bp_name = bp_res["name"]
+    print(f"Pulled details for '{bp_name}'")
+
+
+def test_get_and_delete_token(admin_session: SandboxRestApiSession):
+    """ get token for admin user """
+    token_res = admin_session.get_token_for_target_user("admin")
+    print(f"Token response: '{token_res}'")
+    admin_session.delete_token(token_res)
+    print("deleted token")
diff --git a/tests/test_api_with_sandbox.py b/tests/test_api_with_sandbox.py
new file mode 100644
index 0000000..8c1f97a
--- /dev/null
+++ b/tests/test_api_with_sandbox.py
@@ -0,0 +1,33 @@
+"""
+Test the api methods that require a live sandbox against default
+- start sandbox
+"""
+from common import DEFAULT_BLUEPRINT_TEMPLATE
+from common_fixtures import admin_session
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+
+
+def test_simple_flow(admin_session: SandboxRestApiSession):
+    """ """
+    # start sandbox
+    start_res = admin_session.start_sandbox(blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_name="Pytest simple flow")
+    sandbox_id = start_res["id"]
+    assert sandbox_id
+    print(f"Sandbox started. ID: {sandbox_id}")
+
+    # get sandbox details
+    details_res = admin_session.get_sandbox_details(sandbox_id)
+    sb_name = details_res["name"]
+    print(f"Pulled details for sandbox '{sb_name}'")
+
+    # get components - should be empty
+    sb_components = admin_session.get_sandbox_components(sandbox_id)
+    component_count = len(sb_components)
+    print(f"component count found: {component_count}")
+
+    # get activity events
+
+    # get commands
+
+    #
diff --git a/tests/test_rest_client.py b/tests/test_rest_client.py
deleted file mode 100644
index 0e5af88..0000000
--- a/tests/test_rest_client.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import pytest
-from cloudshell_test.sandbox_rest.sandbox_api import SandboxRestApiClient
-from datetime import datetime
-import json
-from dataclasses import dataclass
-
-DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template"
-DUT_RESOURCE = "DUT_1"
-HEALTH_CHECK_COMMAND = "health_check"
-
-
-@dataclass
-class SandboxTestData:
-    blueprint_name: str
-    sandbox_command: str
-
-
-@dataclass
-class ResourceTestData:
-    resource_name: str
-    resource_command: str
-
-
-@pytest.fixture(name="dut_bp")
-def dut_blueprint_fixture():
-    """ For testing basic operations """
-    return "DUT Test"
-
-
-@pytest.fixture(name="session")
-def rest_session_fixture():
-    return SandboxRestApiClient("localhost", "admin", "admin")
-
-
-def _pretty_print_response(dict_response):
-    json_str = json.dumps(dict_response, indent=4)
-    print(f"\n{json_str}")
-
-
-class TestNoBlueprint:
-    def test_get_sandboxes(self, session: SandboxRestApiClient):
-        res = session.get_sandboxes()
-        _pretty_print_response(res)
-
-
-class TestEmptyBlueprint:
-    bp_data = SandboxTestData(blueprint_name=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_command="Setup")
-
-    def test_simple_flow(self, session: SandboxRestApiClient):
-        start_response = session.start_sandbox(self.bp_data.blueprint_name, f"test sandbox - {datetime.now()}")
-        print(f"\nblueprint {start_response['name']} started")
diff --git a/tests/test_sandbox_controller.py b/tests/test_sandbox_controller.py
new file mode 100644
index 0000000..e69de29

From d5c65f552933e70c6c5286257ecab8763d5f957b Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Sun, 24 Oct 2021 14:28:53 +0300
Subject: [PATCH 13/47] changed init files to remove author

---
 build/lib/cloudshell/__init__.py              |   4 -
 build/lib/cloudshell/sandbox_rest/__init__.py |   4 -
 .../cloudshell/sandbox_rest/flow_helpers.py   |   0
 .../cloudshell/sandbox_rest/sandbox_api.py    | 331 ------------------
 cloudshell/__init__.py                        |   2 -
 cloudshell/sandbox_rest/__init__.py           |   2 -
 .../__pycache__/__init__.cpython-37.pyc       | Bin 283 -> 283 bytes
 .../__pycache__/sandbox_api.cpython-37.pyc    | Bin 17230 -> 17475 bytes
 cloudshell/sandbox_rest/helpers/__init__.py   |   3 +
 cloudshell/sandbox_rest/sandbox_api.py        |  10 +-
 cloudshell/sandbox_rest/sandbox_controller.py |   2 +-
 .../SOURCES.txt                               |   5 +-
 .../requires.txt                              |   1 +
 13 files changed, 14 insertions(+), 350 deletions(-)
 delete mode 100644 build/lib/cloudshell/__init__.py
 delete mode 100644 build/lib/cloudshell/sandbox_rest/__init__.py
 delete mode 100644 build/lib/cloudshell/sandbox_rest/flow_helpers.py
 delete mode 100644 build/lib/cloudshell/sandbox_rest/sandbox_api.py

diff --git a/build/lib/cloudshell/__init__.py b/build/lib/cloudshell/__init__.py
deleted file mode 100644
index cee05d2..0000000
--- a/build/lib/cloudshell/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-__author__ = "quali"
-from pkgutil import extend_path
-
-__path__ = extend_path(__path__, __name__)
diff --git a/build/lib/cloudshell/sandbox_rest/__init__.py b/build/lib/cloudshell/sandbox_rest/__init__.py
deleted file mode 100644
index cee05d2..0000000
--- a/build/lib/cloudshell/sandbox_rest/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-__author__ = "quali"
-from pkgutil import extend_path
-
-__path__ = extend_path(__path__, __name__)
diff --git a/build/lib/cloudshell/sandbox_rest/flow_helpers.py b/build/lib/cloudshell/sandbox_rest/flow_helpers.py
deleted file mode 100644
index e69de29..0000000
diff --git a/build/lib/cloudshell/sandbox_rest/sandbox_api.py b/build/lib/cloudshell/sandbox_rest/sandbox_api.py
deleted file mode 100644
index 6b1bf2b..0000000
--- a/build/lib/cloudshell/sandbox_rest/sandbox_api.py
+++ /dev/null
@@ -1,331 +0,0 @@
-import json
-from dataclasses import asdict, dataclass
-from typing import List
-
-import requests
-
-
-class SandboxRestException(Exception):
-    """ General exception to raise inside Rest client class """
-
-    pass
-
-
-@dataclass
-class InputParam:
-    """ To model the param objects passed to sandbox / component command endpoints """
-
-    name: str
-    value: str
-
-
-class SandboxRestApiClient:
-    """
-    Python wrapper for CloudShell Sandbox API
-    View http://<API_SERVER>/api/v2/explore to see schemas of return json values
-    """
-
-    def __init__(self, host: str, username: str, password: str, domain="Global", port=82, is_https=False, api_version="v2"):
-        """ login to api and store headers for future requests """
-        protocol = "https" if is_https else "http"
-        self._base_url = f"{protocol}://{host}:{port}/api"
-        self._versioned_url = f"{self._base_url}/{api_version}"
-        self._auth_headers = self._get_auth_headers(username, password, domain)
-
-    def _get_auth_headers(self, user_name: str, password: str, domain: str):
-        """
-        Get token from login response, then place token into auth headers on class
-        """
-        login_res = requests.put(
-            url=f"{self._base_url}/login",
-            data=json.dumps({"username": user_name, "password": password, "domain": domain}),
-            headers={"Content-Type": "application/json"},
-        )
-
-        if not login_res.ok:
-            raise SandboxRestException(self._format_err(login_res, "Failed Login"))
-
-        login_token = login_res.text[1:-1]
-
-        auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"}
-        return auth_headers
-
-    @staticmethod
-    def _format_err(response: requests.Response, custom_err_msg="Failed Api Call"):
-        err_msg = f"Response Code: {response.status_code}, Reason: {response.reason}"
-        if custom_err_msg:
-            err_msg = f"{custom_err_msg}. {err_msg}"
-        return err_msg
-
-    # SANDBOX POST REQUESTS
-    def start_sandbox(
-        self,
-        blueprint_id: str,
-        sandbox_name="",
-        duration="PT2H0M",
-        bp_params: List[InputParam] = None,
-        permitted_users: List[str] = None,
-    ):
-        """
-        Create a sandbox from the provided blueprint id
-        Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
-        """
-        url = f"{self._versioned_url}/blueprints/{blueprint_id}/start"
-        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
-
-        data_dict = {
-            "duration": duration,
-            "name": sandbox_name,
-            "permitted_users": permitted_users if permitted_users else [],
-            "params": [asdict(x) for x in bp_params] if bp_params else [],
-        }
-
-        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
-        if not response.ok:
-            err_msg = self._format_err(response, f"Failed to start sandbox from blueprint '{blueprint_id}'")
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    def start_persistent_sandbox(
-        self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None
-    ):
-        """ Create a persistent sandbox from the provided blueprint id """
-        url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent"
-
-        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
-        data_dict = {
-            "name": sandbox_name,
-            "permitted_users": permitted_users if permitted_users else [],
-            "params": [asdict(x) for x in bp_params] if bp_params else [],
-        }
-
-        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
-        if not response.ok:
-            err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'")
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True):
-        """ Run a sandbox level command """
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start"
-        data_dict = {"printOutput": print_output}
-        params = [asdict(x) for x in params] if params else []
-        data_dict["params"] = params
-        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'"))
-        return response.json()
-
-    def run_component_command(
-        self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True
-    ):
-        """ Start a command on sandbox component """
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start"
-        data_dict = {"printOutput": print_output}
-        params = [asdict(x) for x in params] if params else []
-        data_dict["params"] = params
-        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'"))
-        return response.json()
-
-    def extend_sandbox(self, sandbox_id: str, duration: str):
-        """Extend the sandbox
-        :param str sandbox_id: Sandbox id
-        :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
-        :return:
-        """
-        data_dict = {"extended_time": duration}
-        response = requests.post(
-            f"{self._base_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers
-        )
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'"))
-        return response.json()
-
-    def stop_sandbox(self, sandbox_id: str):
-        """ Stop the sandbox given sandbox id """
-        response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'"))
-        return response.json()
-
-    # SANDBOX GET REQUESTS
-    def get_sandboxes(self, show_historic=False):
-        """Get list of sandboxes
-        :return:
-        """
-        params = {"show_historic": show_historic}
-        response = requests.get(f"{self._versioned_url}/sandboxes", headers=self._auth_headers, params=params)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox list"))
-        return response.json()
-
-    def get_sandbox_details(self, sandbox_id: str):
-        """ Get details of the given sandbox id """
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}", headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'"))
-        return response.json()
-
-    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None):
-        """
-        Get list of sandbox activity
-        'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
-        'from_event_id' - integer id of event where to start pulling results from
-        'tail' - how many of the last entries you want to pull
-        'error_only' - to filter for error events only
-        """
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/activity"
-        params = {}
-
-        if error_only:
-            params["error_only"] = error_only
-        if since:
-            params["since"] = since
-        if from_event_id:
-            params["from_event_id"] = from_event_id
-        if tail:
-            params["tail"] = tail
-
-        if params:
-            response = requests.get(url, headers=self._auth_headers, params=params)
-        else:
-            response = requests.get(url, headers=self._auth_headers)
-
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for '{sandbox_id}'"))
-        return response.json()
-
-    def get_sandbox_commands(self, sandbox_id: str):
-        """ Get list of sandbox commands """
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'"))
-        return response.json()
-
-    def get_sandbox_command_details(self, sandbox_id: str, command_name: str):
-        """ Get details of specific sandbox command """
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers
-        )
-        if not response.ok:
-            err_msg = self._format_err(
-                response, f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'"
-            )
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    def get_sandbox_components(self, sandbox_id: str):
-        """ Get list of sandbox components """
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers)
-        if not response.ok:
-            err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'")
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    def get_sandbox_component_details(self, sandbox_id: str, component_id: str):
-        """ Get details of components in sandbox """
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", headers=self._auth_headers
-        )
-        if not response.ok:
-            custom_err_msg = (
-                f"Failed to get sandbox component details for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
-            )
-            err_msg = self._format_err(response, custom_err_msg)
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    def get_sandbox_component_commands(self, sandbox_id: str, component_id: str):
-        """ Get list of commands for a particular component in sandbox """
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers
-        )
-        if not response.ok:
-            custom_err_msg = (
-                f"Failed to get sandbox component commands list for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
-            )
-            err_msg = self._format_err(response, custom_err_msg)
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str):
-        """ Get details of a command of sandbox component """
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            custom_err_msg = (
-                f"Failed to get sandbox component command details for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
-            )
-            err_msg = self._format_err(response, custom_err_msg)
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    def get_sandbox_instructions(self, sandbox_id: str):
-        """ pull the instruction text of sandbox """
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers)
-        if not response.ok:
-            err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'")
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None):
-        """ Get list of sandbox output """
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions"
-        params = {}
-        if tail:
-            params["tail"] = tail
-        if from_entry_id:
-            params["from_entry_id"] = from_entry_id
-        if since:
-            params["since"] = since
-
-        if params:
-            response = requests.get(url, headers=self._auth_headers, params=params)
-        else:
-            response = requests.get(url, headers=self._auth_headers)
-
-        if not response.ok:
-            err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'")
-            raise SandboxRestException(err_msg)
-        return response.json()
-
-    # BLUEPRINT GET REQUESTS
-    def get_blueprints(self):
-        """ Get list of blueprints """
-        response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, "Failed to get blueprints"))
-        return response.json()
-
-    def get_blueprint_details(self, blueprint_id: str):
-        """
-        Get details of a specific blueprint
-        Can pass either blueprint name OR blueprint ID
-        """
-        response = requests.get(f"{self._versioned_url}/blueprints/'{blueprint_id}'")
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'"))
-        return response.json()
-
-    # EXECUTIONS
-    def get_execution_details(self, execution_id: str):
-        response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get execution details for '{execution_id}'"))
-        return response.json()
-
-    def delete_execution(self, execution_id: str):
-        response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to delete execution for '{execution_id}'"))
-        return response.json()
-
-
-if __name__ == "__main__":
-    api = SandboxRestApiClient("localhost", "admin", "admin")
-    bps = api.get_blueprints()
-    pass
diff --git a/cloudshell/__init__.py b/cloudshell/__init__.py
index cee05d2..3ad9513 100644
--- a/cloudshell/__init__.py
+++ b/cloudshell/__init__.py
@@ -1,4 +1,2 @@
-__author__ = "quali"
 from pkgutil import extend_path
-
 __path__ = extend_path(__path__, __name__)
diff --git a/cloudshell/sandbox_rest/__init__.py b/cloudshell/sandbox_rest/__init__.py
index cee05d2..3ad9513 100644
--- a/cloudshell/sandbox_rest/__init__.py
+++ b/cloudshell/sandbox_rest/__init__.py
@@ -1,4 +1,2 @@
-__author__ = "quali"
 from pkgutil import extend_path
-
 __path__ = extend_path(__path__, __name__)
diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc
index 16c0d26a19c161f66f05ab525034462bea0f8f07..eee6736ad4ff2f82925fd57e10453058b115b48f 100644
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc
index 9523de5f9bd52b6931ff603871c27a3ba6e0b6b9..0c82e772dd292885b28a57a3cf3496288ebef2a9 100644
GIT binary patch
diff --git a/cloudshell/sandbox_rest/helpers/__init__.py b/cloudshell/sandbox_rest/helpers/__init__.py
index e69de29..b36383a 100644
--- a/cloudshell/sandbox_rest/helpers/__init__.py
+++ b/cloudshell/sandbox_rest/helpers/__init__.py
@@ -0,0 +1,3 @@
+from pkgutil import extend_path
+
+__path__ = extend_path(__path__, __name__)
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index ae4f970..2b15a88 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -1,8 +1,6 @@
 import json
-import time
 from dataclasses import asdict, dataclass
 from typing import List
-
 import requests
 
 
@@ -428,11 +426,13 @@ def delete_execution(self, execution_id: str):
 
 
 if __name__ == "__main__":
-    TARGET_END_USER = "domain_admin"
     TARGET_END_USER = "natti"
+    TARGET_DOMAIN = "end_users"
+    TARGET_DOMAIN = "Global"
 
-    admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin")
-
+    # admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN)
+    admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN)
+    from cloudshell.api.cloudshell_api import CloudShellAPISession
     with admin_api:
         all_sandboxes = admin_api.get_sandboxes()
         print(f"Sandbox count pulled by system admin: {len(all_sandboxes)}")
diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py
index 473c7c5..46fae1a 100644
--- a/cloudshell/sandbox_rest/sandbox_controller.py
+++ b/cloudshell/sandbox_rest/sandbox_controller.py
@@ -12,7 +12,7 @@
 )
 from cloudshell.sandbox_rest.sandbox_api import InputParam, SandboxRestApiSession
 from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents
-
+from cloudshell.logger.qs_logger import get_qs_logger
 
 class SandboxSetupError(Exception):
     """ When sandbox has error during setup """
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
index e9351d7..1998237 100644
--- a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
+++ b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
@@ -2,8 +2,11 @@ README.rst
 setup.py
 cloudshell/__init__.py
 cloudshell/sandbox_rest/__init__.py
-cloudshell/sandbox_rest/flow_helpers.py
 cloudshell/sandbox_rest/sandbox_api.py
+cloudshell/sandbox_rest/sandbox_components.py
+cloudshell/sandbox_rest/sandbox_controller.py
+cloudshell/sandbox_rest/helpers/__init__.py
+cloudshell/sandbox_rest/helpers/polling_helpers.py
 cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
 cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
 cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt
index f229360..c3e8e97 100644
--- a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt
+++ b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt
@@ -1 +1,2 @@
 requests
+retrying

From 7dd06cd9de621421034123b22cee553ed39d7a43 Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Sun, 24 Oct 2021 14:29:22 +0300
Subject: [PATCH 14/47] testing init changes

---
 cloudshell/__init__.py                        | 1 +
 cloudshell/sandbox_rest/__init__.py           | 1 +
 cloudshell/sandbox_rest/sandbox_api.py        | 2 ++
 cloudshell/sandbox_rest/sandbox_controller.py | 3 ++-
 4 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/cloudshell/__init__.py b/cloudshell/__init__.py
index 3ad9513..b36383a 100644
--- a/cloudshell/__init__.py
+++ b/cloudshell/__init__.py
@@ -1,2 +1,3 @@
 from pkgutil import extend_path
+
 __path__ = extend_path(__path__, __name__)
diff --git a/cloudshell/sandbox_rest/__init__.py b/cloudshell/sandbox_rest/__init__.py
index 3ad9513..b36383a 100644
--- a/cloudshell/sandbox_rest/__init__.py
+++ b/cloudshell/sandbox_rest/__init__.py
@@ -1,2 +1,3 @@
 from pkgutil import extend_path
+
 __path__ = extend_path(__path__, __name__)
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 2b15a88..97b090f 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -1,6 +1,7 @@
 import json
 from dataclasses import asdict, dataclass
 from typing import List
+
 import requests
 
 
@@ -433,6 +434,7 @@ def delete_execution(self, execution_id: str):
     # admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN)
     admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN)
     from cloudshell.api.cloudshell_api import CloudShellAPISession
+
     with admin_api:
         all_sandboxes = admin_api.get_sandboxes()
         print(f"Sandbox count pulled by system admin: {len(all_sandboxes)}")
diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py
index 46fae1a..bc79418 100644
--- a/cloudshell/sandbox_rest/sandbox_controller.py
+++ b/cloudshell/sandbox_rest/sandbox_controller.py
@@ -2,6 +2,7 @@
 from timeit import default_timer
 from typing import List
 
+from cloudshell.logger.qs_logger import get_qs_logger
 from cloudshell.sandbox_rest.helpers.polling_helpers import (
     SANDBOX_ACTIVE_STATES,
     SANDBOX_SETUP_STATES,
@@ -12,7 +13,7 @@
 )
 from cloudshell.sandbox_rest.sandbox_api import InputParam, SandboxRestApiSession
 from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents
-from cloudshell.logger.qs_logger import get_qs_logger
+
 
 class SandboxSetupError(Exception):
     """ When sandbox has error during setup """

From c67b632e2d20308df568c3568319bd93675d449f Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Tue, 26 Oct 2021 02:10:38 +0300
Subject: [PATCH 15/47] refactored exception formatting and added tests

---
 .gitignore                                    |   3 +-
 .../sandbox_components.cpython-37.pyc         | Bin 5053 -> 0 bytes
 cloudshell/sandbox_rest/exceptions.py         |  28 +++
 cloudshell/sandbox_rest/helpers/__init__.py   |   3 -
 .../__pycache__/__init__.cpython-37.pyc       | Bin 191 -> 0 bytes
 .../polling_helpers.cpython-37.pyc            | Bin 4240 -> 0 bytes
 .../sandbox_rest/helpers/polling_helpers.py   | 131 ----------
 cloudshell/sandbox_rest/sandbox_api.py        | 228 +++++++-----------
 cloudshell/sandbox_rest/sandbox_components.py | 102 --------
 cloudshell/sandbox_rest/sandbox_controller.py | 185 --------------
 .../PKG-INFO                                  |  58 ++---
 .../SOURCES.txt                               |   6 +-
 test-requirements.txt                         |   3 +-
 tests/common.py                               |   4 -
 tests/common_fixtures.py                      |  20 --
 tests/conftest.py                             |  14 ++
 tests/env_settings.py                         |  22 ++
 tests/test_api _no_sandbox.py                 |   6 +-
 tests/test_api_empty_sandbox.py               |  67 +++++
 tests/test_api_with_sandbox.py                |  33 ---
 tests/test_sandbox_controller.py              |   0
 21 files changed, 262 insertions(+), 651 deletions(-)
 delete mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc
 create mode 100644 cloudshell/sandbox_rest/exceptions.py
 delete mode 100644 cloudshell/sandbox_rest/helpers/__init__.py
 delete mode 100644 cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc
 delete mode 100644 cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc
 delete mode 100644 cloudshell/sandbox_rest/helpers/polling_helpers.py
 delete mode 100644 cloudshell/sandbox_rest/sandbox_components.py
 delete mode 100644 cloudshell/sandbox_rest/sandbox_controller.py
 delete mode 100644 tests/common_fixtures.py
 create mode 100644 tests/conftest.py
 create mode 100644 tests/env_settings.py
 create mode 100644 tests/test_api_empty_sandbox.py
 delete mode 100644 tests/test_api_with_sandbox.py
 delete mode 100644 tests/test_sandbox_controller.py

diff --git a/.gitignore b/.gitignore
index 8750fc0..25cf4a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 .idea/
 .pytest_cache/
-*.egg
\ No newline at end of file
+*.egg
+tests/.env
\ No newline at end of file
diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_components.cpython-37.pyc
deleted file mode 100644
index 1840cfc278748ee38bd9e3c24ab13b2c07d34784..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py
new file mode 100644
index 0000000..634586d
--- /dev/null
+++ b/cloudshell/sandbox_rest/exceptions.py
@@ -0,0 +1,28 @@
+from requests import Response
+
+
+class SandboxRestException(Exception):
+    """ Base Exception Class inside Rest client class """
+    def __init__(self, msg: str, response: Response = None):
+        self.msg = msg
+        self.response = response
+
+    def __str__(self):
+        return self._format_err_msg(self.msg, self.response)
+
+    def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = None) -> str:
+        err_msg = f"Sandbox API Error: {custom_err_msg}"
+
+        if response:
+            err_msg += f"\n{self._format_response_msg(response)}"
+
+    @staticmethod
+    def _format_response_msg(response: Response):
+        return (f"Response: {response.status_code}, Reason: {response.reason}\n"
+                f"Request URL: {response.request.url}\n"
+                f"Request Headers: {response.request.headers}")
+
+
+class SandboxRestAuthException(SandboxRestException):
+    """ Failed auth action """
+    pass
diff --git a/cloudshell/sandbox_rest/helpers/__init__.py b/cloudshell/sandbox_rest/helpers/__init__.py
deleted file mode 100644
index b36383a..0000000
--- a/cloudshell/sandbox_rest/helpers/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from pkgutil import extend_path
-
-__path__ = extend_path(__path__, __name__)
diff --git a/cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/helpers/__pycache__/__init__.cpython-37.pyc
deleted file mode 100644
index 2b20b3f99c96cfe3923b9169a2ea40bf89947b6b..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc b/cloudshell/sandbox_rest/helpers/__pycache__/polling_helpers.cpython-37.pyc
deleted file mode 100644
index 18e0a2b4aa9cb546cee1bcb3c1a4cdae242d1c9f..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/cloudshell/sandbox_rest/helpers/polling_helpers.py b/cloudshell/sandbox_rest/helpers/polling_helpers.py
deleted file mode 100644
index 531da57..0000000
--- a/cloudshell/sandbox_rest/helpers/polling_helpers.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""
-Helper functions to poll the state of sandbox setup or the execution status of a command.
-Using 'retrying' library to do polling:
-https://pypi.org/project/retrying/
-"""
-from enum import Enum
-from typing import Callable, List
-
-from retrying import RetryError, retry  # pip install retrying
-
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
-
-
-class OrchestrationPollingTimeout(Exception):
-    pass
-
-
-class CommandPollingTimeout(Exception):
-    pass
-
-
-class SandboxStates(Enum):
-    BEFORE_SETUP_STATE = "BeforeSetup"
-    RUNNING_SETUP_STATE = "Setup"
-    error_state = "Error"
-    ready_state = "Ready"
-    teardown_state = "Teardown"
-    ended_state = "Ended"
-
-
-class ExecutionStatuses(Enum):
-    running_status = "Running"
-    pending_status = "Pending"
-    completed_status = "Completed"
-    failed_status = "Failed"
-
-
-SANDBOX_SETUP_STATES = [SandboxStates.BEFORE_SETUP_STATE.value, SandboxStates.RUNNING_SETUP_STATE.value]
-
-SANDBOX_ACTIVE_STATES = [SandboxStates.ready_state.value, SandboxStates.error_state.value]
-
-UNFINISHED_EXECUTION_STATUSES = [ExecutionStatuses.running_status.value, ExecutionStatuses.pending_status.value]
-
-
-def _should_we_keep_polling_setup(sandbox_details: dict) -> bool:
-    """ if still in setup keep polling """
-    setup_states = [SandboxStates.BEFORE_SETUP_STATE.value, SandboxStates.RUNNING_SETUP_STATE.value]
-    if sandbox_details["state"] in setup_states:
-        return True
-    return False
-
-
-def _should_we_keep_polling_teardown(sandbox_details: dict) -> bool:
-    """ if in teardown keep polling """
-    if sandbox_details["state"] == SandboxStates.teardown_state.value:
-        return True
-    return False
-
-
-def _poll_sandbox_state(
-    api: SandboxRestApiSession,
-    reservation_id: str,
-    polling_func: Callable,
-    max_polling_minutes: int,
-    polling_frequency_seconds: int,
-) -> str:
-    """ Create blocking polling process """
-
-    # retry wait times are in milliseconds
-    @retry(
-        retry_on_result=polling_func, wait_fixed=polling_frequency_seconds * 1000, stop_max_delay=max_polling_minutes * 60000
-    )
-    def get_sandbox_details():
-        return api.get_sandbox_details(reservation_id)
-
-    try:
-        sandbox_details = get_sandbox_details()
-    except RetryError:
-        raise OrchestrationPollingTimeout(f"Sandbox Polling timed out after configured {max_polling_minutes} minutes")
-    return sandbox_details
-
-
-def poll_sandbox_setup(
-    api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30
-) -> dict:
-    """ poll until completion """
-    sandbox_details = _poll_sandbox_state(
-        api, reservation_id, _should_we_keep_polling_setup, max_polling_minutes, polling_frequency_seconds
-    )
-    return sandbox_details
-
-
-def poll_sandbox_teardown(
-    api: SandboxRestApiSession, reservation_id: str, max_polling_minutes=20, polling_frequency_seconds=30
-) -> dict:
-    """ poll until completion  """
-    sandbox_details = _poll_sandbox_state(
-        api, reservation_id, _should_we_keep_polling_teardown, max_polling_minutes, polling_frequency_seconds
-    )
-    return sandbox_details
-
-
-def _should_we_keep_polling_execution(exc_data: dict) -> bool:
-    current_exc_status = exc_data["status"]
-    if current_exc_status in UNFINISHED_EXECUTION_STATUSES:
-        return True
-    return False
-
-
-def poll_execution_for_completion(
-    sandbox_rest: SandboxRestApiSession, command_execution_id: str, max_polling_in_minutes=20, polling_frequency_in_seconds=30
-) -> str:
-    """
-    poll execution for "Completed" status, then return the execution output
-    """
-
-    # retry wait times are in milliseconds
-    @retry(
-        retry_on_result=_should_we_keep_polling_execution,
-        wait_fixed=polling_frequency_in_seconds * 1000,
-        stop_max_delay=max_polling_in_minutes * 60000,
-    )
-    def get_execution_data():
-        exc_data = sandbox_rest.get_execution_details(command_execution_id)
-        return exc_data
-
-    try:
-        exc_data = get_execution_data(sandbox_rest, command_execution_id)
-    except RetryError:
-        raise CommandPollingTimeout(f"Execution polling timed out after max {max_polling_in_minutes} minutes")
-    return exc_data
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 97b090f..0a5cd18 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -1,26 +1,8 @@
 import json
 from dataclasses import asdict, dataclass
 from typing import List
-
 import requests
-
-
-class SandboxRestException(Exception):
-    """ General exception to raise inside Rest client class """
-
-    pass
-
-
-class SandboxRestAuthException(Exception):
-    """ Failed auth actions """
-
-    pass
-
-
-class SandboxRestInitException(ValueError):
-    """ Failed auth actions """
-
-    pass
+from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException
 
 
 @dataclass
@@ -40,9 +22,8 @@ class SandboxRestApiSession:
     View http://<API_SERVER>/api/v2/explore to see schemas of return json values
     """
 
-    def __init__(
-        self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, api_version="v2"
-    ):
+    def __init__(self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False,
+                 api_version="v2"):
         """ login to api and store headers for future requests """
         _protocol = "https" if is_https else "http"
         self._base_url = f"{_protocol}://{host}:{port}/api"
@@ -73,15 +54,15 @@ def _build_auth_headers(login_token: str) -> dict:
         auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"}
         return auth_headers
 
-    def refresh_auth_from_stored_credentials(self):
+    def refresh_auth_from_stored_credentials(self) -> None:
         self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain)
         self._auth_headers = self._build_auth_headers(self.auth_token)
 
-    def invalidate_auth(self):
+    def invalidate_auth(self) -> None:
         self.delete_token(self.auth_token)
         self._auth_headers = None
 
-    def _validate_auth_headers(self):
+    def _validate_auth_headers(self) -> None:
         if not self._auth_headers:
             raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session")
 
@@ -90,16 +71,16 @@ def get_token_with_credentials(self, user_name: str, password: str, domain: str)
         """
         Get token from credentials - extraneous quotes stripped off token string
         """
-        login_res = requests.put(
+        response = requests.put(
             url=f"{self._base_url}/login",
             data=json.dumps({"username": user_name, "password": password, "domain": domain}),
             headers={"Content-Type": "application/json"},
         )
 
-        if not login_res.ok:
-            raise SandboxRestAuthException(self._format_err(login_res, "Failed Login"))
+        if not response.ok:
+            raise SandboxRestAuthException("Failed Login", response)
 
-        login_token = login_res.text[1:-1]
+        login_token = response.text[1:-1]
         return login_token
 
     def get_token_for_target_user(self, user_name: str) -> str:
@@ -107,38 +88,30 @@ def get_token_for_target_user(self, user_name: str) -> str:
         Get token for target user - remove extraneous quotes
         """
         self._validate_auth_headers()
-        login_res = requests.post(
+        response = requests.post(
             url=f"{self._base_url}/token", data=json.dumps({"username": user_name}), headers=self._auth_headers
         )
 
-        if not login_res.ok:
-            raise SandboxRestException(self._format_err(login_res, f"Failed to get get token for user {user_name}"))
+        if not response.ok:
+            raise SandboxRestException(f"Failed to get get token for user {user_name}", response)
 
-        login_token = login_res.text[1:-1]
+        login_token = response.text[1:-1]
         return login_token
 
     def delete_token(self, token_id: str) -> None:
         self._validate_auth_headers()
-        login_res = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers)
-        if not login_res.ok:
-            raise SandboxRestException(self._format_err(login_res, "Failed to delete token"))
-
-    def _format_err(self, response: requests.Response, custom_err_msg="Failed Api Call"):
-        response_data = (
-            f"Response: {response.status_code}, Reason: {response.reason}\n"
-            f"Request URL: {response.request.url}\n"
-            f"Request Headers: {response.request.headers}"
-        )
-        return f"Sandbox API Error for User: '{self.username}'.\n" f"{custom_err_msg}\n" f"{response_data}"
+        response = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers)
+        if not response.ok:
+            raise SandboxRestException("Failed to delete token", response)
 
     # SANDBOX POST REQUESTS
     def start_sandbox(
-        self,
-        blueprint_id: str,
-        sandbox_name="",
-        duration="PT2H0M",
-        bp_params: List[InputParam] = None,
-        permitted_users: List[str] = None,
+            self,
+            blueprint_id: str,
+            sandbox_name="",
+            duration="PT2H0M",
+            bp_params: List[InputParam] = None,
+            permitted_users: List[str] = None,
     ) -> dict:
         """
         Create a sandbox from the provided blueprint id
@@ -162,13 +135,13 @@ def start_sandbox(
                 custom_msg += "Ensure blueprint is 'Public' and in correct domain."
             if response.status_code >= 500:
                 custom_msg += "Internal Server 500 Error During Request."
-            err_msg = self._format_err(response, custom_msg)
-            raise SandboxRestException(err_msg)
+            raise SandboxRestException(custom_msg, response)
         return response.json()
 
     def start_persistent_sandbox(
-        self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None
-    ):
+            self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None,
+            permitted_users: List[str] = None
+    ) -> dict:
         """ Create a persistent sandbox from the provided blueprint id """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent"
@@ -182,11 +155,12 @@ def start_persistent_sandbox(
 
         response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
         if not response.ok:
-            err_msg = self._format_err(response, f"Failed to start persistent sandbox from blueprint '{blueprint_id}'")
-            raise SandboxRestException(err_msg)
+            err_msg = f"Failed to start persistent sandbox from blueprint '{blueprint_id}'"
+            raise SandboxRestException(err_msg, response)
         return response.json()
 
-    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True):
+    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None,
+                            print_output=True) -> dict:
         """ Run a sandbox level command """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start"
@@ -195,12 +169,13 @@ def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[I
         data_dict["params"] = params
         response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to start sandbox command '{command_name}'"))
+            raise SandboxRestException(f"failed to start sandbox command '{command_name}'", response)
         return response.json()
 
     def run_component_command(
-        self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True
-    ):
+            self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None,
+            print_output: bool = True
+    ) -> dict:
         """ Start a command on sandbox component """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start"
@@ -209,10 +184,10 @@ def run_component_command(
         data_dict["params"] = params
         response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to start component command '{command_name}'"))
+            raise SandboxRestException(f"failed to start component command '{command_name}'", response)
         return response.json()
 
-    def extend_sandbox(self, sandbox_id: str, duration: str):
+    def extend_sandbox(self, sandbox_id: str, duration: str) -> dict:
         """Extend the sandbox
         :param str sandbox_id: Sandbox id
         :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
@@ -221,42 +196,43 @@ def extend_sandbox(self, sandbox_id: str, duration: str):
         self._validate_auth_headers()
         data_dict = {"extended_time": duration}
         response = requests.post(
-            f"{self._base_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers
         )
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"failed to extend sandbox '{sandbox_id}'"))
+            raise SandboxRestException(f"failed to extend sandbox '{sandbox_id}'", response)
         return response.json()
 
-    def stop_sandbox(self, sandbox_id: str):
+    def stop_sandbox(self, sandbox_id: str) -> None:
         """ Stop the sandbox given sandbox id """
         self._validate_auth_headers()
         response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to stop sandbox '{sandbox_id}'"))
+            raise SandboxRestException(f"Failed to stop sandbox '{sandbox_id}'", response)
 
     # SANDBOX GET REQUESTS
-    def get_sandboxes(self, show_historic=False):
+    def get_sandboxes(self, show_historic=False) -> list:
         """ Get list of sandboxes """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes"
         params = {"show_historic": "true" if show_historic else "false"}
         response = requests.get(url, headers=self._auth_headers, params=params)
         if not response.ok:
-            err_msg = self._format_err(response, "Failed to get sandbox list")
-            raise SandboxRestException(err_msg)
+            err_msg = "Failed to get sandboxes"
+            raise SandboxRestException(err_msg, response)
         return response.json()
 
-    def get_sandbox_details(self, sandbox_id: str):
+    def get_sandbox_details(self, sandbox_id: str) -> dict:
         """ Get details of the given sandbox id """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}"
         response = requests.get(url, headers=self._auth_headers)
         if not response.ok:
-            exc_msg = self._format_err(response, f"Failed to get sandbox details for '{sandbox_id}'")
-            raise SandboxRestException(exc_msg)
+            exc_msg = f"Failed to get sandbox details for '{sandbox_id}'"
+            raise SandboxRestException(exc_msg, response)
         return response.json()
 
-    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None):
+    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None,
+                             tail: int = None) -> dict:
         """
         Get list of sandbox activity
         'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
@@ -283,40 +259,38 @@ def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from
             response = requests.get(url, headers=self._auth_headers)
 
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox activity for '{sandbox_id}'"))
+            raise SandboxRestException(f"Failed to get sandbox activity for '{sandbox_id}'", response)
         return response.json()
 
-    def get_sandbox_commands(self, sandbox_id: str):
+    def get_sandbox_commands(self, sandbox_id: str) -> list:
         """ Get list of sandbox commands """
         self._validate_auth_headers()
         response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get sandbox commands for '{sandbox_id}'"))
+            raise SandboxRestException(f"Failed to get sandbox commands for '{sandbox_id}'", response)
         return response.json()
 
-    def get_sandbox_command_details(self, sandbox_id: str, command_name: str):
+    def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dict:
         """ Get details of specific sandbox command """
         self._validate_auth_headers()
         response = requests.get(
             f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers
         )
         if not response.ok:
-            err_msg = self._format_err(
-                response, f"Failed to get sandbox command details for '{command_name}' in '{sandbox_id}'"
-            )
-            raise SandboxRestException(err_msg)
+            err_msg = f"Failed to get command details for '{command_name}' in '{sandbox_id}'"
+            raise SandboxRestException(err_msg, response)
         return response.json()
 
-    def get_sandbox_components(self, sandbox_id: str):
+    def get_sandbox_components(self, sandbox_id: str) -> list:
         """ Get list of sandbox components """
         self._validate_auth_headers()
         response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers)
         if not response.ok:
-            err_msg = self._format_err(response, f"Failed to get sandbox components details for '{sandbox_id}'")
-            raise SandboxRestException(err_msg)
+            err_msg = (response, f"Failed to get components for '{sandbox_id}'")
+            raise SandboxRestException(err_msg, response)
         return response.json()
 
-    def get_sandbox_component_details(self, sandbox_id: str, component_id: str):
+    def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> dict:
         """ Get details of components in sandbox """
         self._validate_auth_headers()
         response = requests.get(
@@ -324,27 +298,29 @@ def get_sandbox_component_details(self, sandbox_id: str, component_id: str):
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get sandbox component details for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
+                f"Failed to get sandbox component details.\n"
+                f"component id: '{component_id}'\n"
+                f"sandbox id: '{sandbox_id}'"
             )
-            err_msg = self._format_err(response, custom_err_msg)
-            raise SandboxRestException(err_msg)
+            raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
-    def get_sandbox_component_commands(self, sandbox_id: str, component_id: str):
+    def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> list:
         """ Get list of commands for a particular component in sandbox """
         self._validate_auth_headers()
         response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands",
+            headers=self._auth_headers
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get sandbox component commands list for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
-            )
-            err_msg = self._format_err(response, custom_err_msg)
-            raise SandboxRestException(err_msg)
+                f"Failed to get component commands.\n"
+                f"component id: '{component_id}'\n"
+                f"sandbox id: '{sandbox_id}'")
+            raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
-    def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str):
+    def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str) -> dict:
         """ Get details of a command of sandbox component """
         self._validate_auth_headers()
         response = requests.get(
@@ -352,26 +328,27 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s
             headers=self._auth_headers,
         )
         if not response.ok:
-            custom_err_msg = (
-                f"Failed to get sandbox component command details for component: '{component_id}', " f"sandbox: '{sandbox_id}'"
-            )
-            err_msg = self._format_err(response, custom_err_msg)
-            raise SandboxRestException(err_msg)
+            custom_err_msg = (f"Failed to get command details.\n "
+                              f"component id: '{component_id}'\n"
+                              f"sandbox id: '{sandbox_id}'")
+            raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
-    def get_sandbox_instructions(self, sandbox_id: str):
+    def get_sandbox_instructions(self, sandbox_id: str) -> str:
         """ pull the instruction text of sandbox """
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions",
+                                headers=self._auth_headers)
         if not response.ok:
-            err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'")
-            raise SandboxRestException(err_msg)
+            err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'"
+            raise SandboxRestException(err_msg, response)
         return response.json()
 
-    def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None):
+    def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None,
+                           since: str = None) -> dict:
         """ Get list of sandbox output """
         self._validate_auth_headers()
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions"
+        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/output"
         params = {}
         if tail:
             params["tail"] = tail
@@ -386,20 +363,20 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i
             response = requests.get(url, headers=self._auth_headers)
 
         if not response.ok:
-            err_msg = self._format_err(response, f"Failed to get sandbox instructions for '{sandbox_id}'")
-            raise SandboxRestException(err_msg)
+            err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'"
+            raise SandboxRestException(err_msg, response)
         return response.json()
 
     # BLUEPRINT GET REQUESTS
-    def get_blueprints(self):
+    def get_blueprints(self) -> list:
         """ Get list of blueprints """
         self._validate_auth_headers()
         response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, "Failed to get blueprints"))
+            raise SandboxRestException("Failed to get blueprints", response)
         return response.json()
 
-    def get_blueprint_details(self, blueprint_id: str):
+    def get_blueprint_details(self, blueprint_id: str) -> dict:
         """
         Get details of a specific blueprint
         Can pass either blueprint name OR blueprint ID
@@ -407,39 +384,20 @@ def get_blueprint_details(self, blueprint_id: str):
         self._validate_auth_headers()
         response = requests.get(f"{self._versioned_url}/blueprints/{blueprint_id}", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get blueprint data for '{blueprint_id}'"))
+            raise SandboxRestException(f"Failed to get blueprint data for '{blueprint_id}'", response)
         return response.json()
 
     # EXECUTIONS
-    def get_execution_details(self, execution_id: str):
+    def get_execution_details(self, execution_id: str) -> dict:
         self._validate_auth_headers()
         response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to get execution details for '{execution_id}'"))
+            raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response)
         return response.json()
 
-    def delete_execution(self, execution_id: str):
+    def delete_execution(self, execution_id: str) -> dict:
         self._validate_auth_headers()
         response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(self._format_err(response, f"Failed to delete execution for '{execution_id}'"))
+            raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response)
         return response.json()
-
-
-if __name__ == "__main__":
-    TARGET_END_USER = "natti"
-    TARGET_DOMAIN = "end_users"
-    TARGET_DOMAIN = "Global"
-
-    # admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN)
-    admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain=TARGET_DOMAIN)
-    from cloudshell.api.cloudshell_api import CloudShellAPISession
-
-    with admin_api:
-        all_sandboxes = admin_api.get_sandboxes()
-        print(f"Sandbox count pulled by system admin: {len(all_sandboxes)}")
-
-        end_user_token = admin_api.get_token_for_target_user(TARGET_END_USER)
-        with SandboxRestApiSession(host="localhost", username=TARGET_END_USER, token=end_user_token) as user_rest:
-            user_bps = user_rest.get_blueprints()
-            print(f"Blueprint count pulled by user '{TARGET_END_USER}': {len(user_bps)}")
diff --git a/cloudshell/sandbox_rest/sandbox_components.py b/cloudshell/sandbox_rest/sandbox_components.py
deleted file mode 100644
index 7325eab..0000000
--- a/cloudshell/sandbox_rest/sandbox_components.py
+++ /dev/null
@@ -1,102 +0,0 @@
-import json
-from enum import Enum
-from typing import List
-
-
-class ComponentTypes(Enum):
-    app_type = "Application"
-    resource_type = "Resource"
-    service_type = "Service"
-
-
-class AppLifeCycleTypes(Enum):
-    deployed = "Deployed"
-    un_deployed = "Undeployed"
-
-
-class AttributeTypes(Enum):
-    boolean_type = "boolean"
-    password_type = "password"
-    string_type = "string"
-    numeric_type = "numeric"
-
-
-class SandboxRestComponents:
-    def __init__(self, components: List[dict] = None):
-        """
-        if instantiated with components then populate lists
-        if not, then call "refresh_components" after getting data from sandbox
-        """
-        self.all_components = components if components else []
-        self.services = []
-        self.resources = []
-        self.deployed_apps = []
-        self.un_deployed_apps = []
-        if self.all_components:
-            self._sort_components()
-
-    def _filter_components_by_type(self, component_type: str) -> List[dict]:
-        """ accepts both short info components and full info """
-        return [component for component in self.all_components if component["type"] == component_type]
-
-    def _filter_app_by_lifecycle(self, lifecycle_type):
-        return [
-            component
-            for component in self.all_components
-            if component["type"] == ComponentTypes.app_type.value and component["app_lifecyle"] == lifecycle_type
-        ]
-
-    def _sort_components(self) -> None:
-        """ sort stored components into separate lists """
-        self.resources = self._filter_components_by_type(ComponentTypes.resource_type.value)
-        self.services = self._filter_components_by_type(ComponentTypes.service_type.value)
-        self.deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.deployed.value)
-        self.un_deployed_apps = self._filter_app_by_lifecycle(AppLifeCycleTypes.un_deployed.value)
-
-    def refresh_components(self, components: List[dict]) -> None:
-        self.all_components = components
-        self._sort_components()
-
-    def find_component_by_name(self, component_name: str) -> dict:
-        for component in self.all_components:
-            if component["name"] == component_name:
-                return component
-
-    @staticmethod
-    def filter_by_model(components: List[dict], model: str) -> List[dict]:
-        """
-        Can pass in all components or sub list
-        Both Resources and Applications can use same shell / model
-        """
-        return [component for component in components if component["component_type"] == model]
-
-    def filter_by_attr_value(self, components: List[dict], attribute_name: str, attribute_value: str) -> List[dict]:
-        """ attribute name does not have to include model / family namespace """
-        self._validate_components_for_attributes(components)
-        result = []
-        for component in components:
-            for attr in component["attributes"]:
-                if attr["name"].endswith(attribute_name) and attr["value"] == attribute_value:
-                    result.append(component)
-        return result
-
-    def filter_by_boolean_attr_true(self, components: List[dict], attribute_name: str) -> List[dict]:
-        """ attribute name does not have to include model / family namespace """
-        self._validate_components_for_attributes(components)
-        result = []
-        for component in components:
-            for attr in component["attributes"]:
-                if attr["name"].endswith(attribute_name) and attr["value"].lower() == "true":
-                    result.append(component)
-        return result
-
-    @staticmethod
-    def _validate_components_for_attributes(components: List[dict]):
-        if not components:
-            return
-        attrs = components[0].get("attributes")
-        if not attrs:
-            raise Exception(
-                "'attributes' member not found. Must pass in Full info components.\n"
-                f"components data passed:\n{json.dumps(components, indent=4)}"
-            )
diff --git a/cloudshell/sandbox_rest/sandbox_controller.py b/cloudshell/sandbox_rest/sandbox_controller.py
deleted file mode 100644
index bc79418..0000000
--- a/cloudshell/sandbox_rest/sandbox_controller.py
+++ /dev/null
@@ -1,185 +0,0 @@
-import json
-from timeit import default_timer
-from typing import List
-
-from cloudshell.logger.qs_logger import get_qs_logger
-from cloudshell.sandbox_rest.helpers.polling_helpers import (
-    SANDBOX_ACTIVE_STATES,
-    SANDBOX_SETUP_STATES,
-    SandboxStates,
-    poll_execution_for_completion,
-    poll_sandbox_setup,
-    poll_sandbox_teardown,
-)
-from cloudshell.sandbox_rest.sandbox_api import InputParam, SandboxRestApiSession
-from cloudshell.sandbox_rest.sandbox_components import SandboxRestComponents
-
-
-class SandboxSetupError(Exception):
-    """ When sandbox has error during setup """
-
-    pass
-
-
-class SandboxTeardownError(Exception):
-    """ When sandbox has error during setup """
-
-    pass
-
-
-class CommandFailedException(Exception):
-    pass
-
-
-class SandboxRestController:
-    def __init__(self, api: SandboxRestApiSession, sandbox_id="", disable_prints=False):
-        self.api = api
-        self.sandbox_id = sandbox_id
-        self.components = SandboxRestComponents()
-        self.setup_finished = False
-        self.sandbox_ended = False
-        self.setup_errors: List[dict] = None
-        self.teardown_errors: List[dict] = None
-        self.disable_prints = disable_prints
-
-        # when passing in existing sandbox id update instance state, otherwise let "start sandbox" handle it
-        if self.sandbox_id:
-            self._handle_sandbox_id_on_init()
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        if exc_type:
-            err_msg = f"Exiting sandbox scope with error. f{exc_type}: {exc_val}"
-            self._print(err_msg)
-
-    def _handle_sandbox_id_on_init(self):
-        self.update_components()
-        self.update_state_flags()
-
-    def update_components(self):
-        components = self.api.get_sandbox_components(self.sandbox_id)
-        self.components.refresh_components(components)
-
-    def update_state_flags(self):
-        details = self.api.get_sandbox_details(self.sandbox_id)
-        sandbox_state = details["state"]
-        if sandbox_state not in SANDBOX_SETUP_STATES:
-            self.setup_finished = True
-        elif sandbox_state not in SANDBOX_ACTIVE_STATES:
-            self.sandbox_ended = True
-
-    def _print(self, message):
-        if not self.disable_prints:
-            print(message)
-
-    def start_sandbox_and_poll(
-        self,
-        blueprint_id: str,
-        sandbox_name="",
-        duration="PT1H30M",
-        bp_params: List[InputParam] = None,
-        permitted_users: List[str] = None,
-        max_polling_minutes=30,
-        raise_setup_exception=True,
-    ) -> dict:
-        """ Start sandbox, poll for result, get back sandbox info """
-        if self.sandbox_id:
-            raise ValueError(
-                f"Sandbox already has id '{self.sandbox_id}'.\n" f"Start blueprint with new sandbox controller instance"
-            )
-
-        self._print(f"Starting blueprint {blueprint_id}")
-        start = default_timer()
-        start_response = self.api.start_sandbox(blueprint_id, sandbox_name, duration, bp_params, permitted_users)
-        self.sandbox_id = start_response["id"]
-        sandbox_details = poll_sandbox_setup(self.api, self.sandbox_id, max_polling_minutes, polling_frequency_seconds=30)
-        self.update_components()
-        sandbox_state = sandbox_details["state"]
-        stage = sandbox_details["setup_stage"]
-        name = sandbox_details["name"]
-
-        if sandbox_state == SandboxStates.ready_state.value:
-            self.setup_finished = True
-
-        # scan activity feed for errors
-        if sandbox_state == SandboxStates.error_state.value:
-            self.setup_finished = True
-            activity_errors = self._get_all_activity_errors(self.sandbox_id)
-            if activity_errors:
-                # print and store setup error data
-                self.setup_errors = activity_errors
-                err_msg = f"Error Events during setup:\n{json.dumps(activity_errors, indent=4)}"
-                self._print(err_msg)
-
-            if raise_setup_exception:
-                err_msg = (
-                    f"Sandbox '{name}' Error during SETUP. See events for details.\n"
-                    f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{self.sandbox_id}'"
-                )
-                raise SandboxSetupError(err_msg)
-
-        total_minutes = (default_timer() - start) / 60
-        self._print(f"Setup finished after {total_minutes:.2f} minutes.")
-        return sandbox_details
-
-    def stop_sandbox_and_poll(self, sandbox_id: str, max_polling_minutes=30, raise_teardown_exception=True) -> dict:
-        if self.sandbox_ended:
-            raise ValueError(f"sandbox {self.sandbox_id} already in completed state")
-
-        last_activity_id = self.api.get_sandbox_activity(self.sandbox_id, tail=1)["events"]["id"]
-        start = default_timer()
-        self.api.stop_sandbox(sandbox_id)
-        sandbox_details = poll_sandbox_teardown(self.api, sandbox_id, max_polling_minutes, polling_frequency_seconds=30)
-        self.update_components()
-        sandbox_state = sandbox_details["state"]
-        stage = sandbox_details["setup_stage"]
-        name = sandbox_details["name"]
-
-        if sandbox_state == SandboxStates.ended_state.value:
-            self.sandbox_ended = True
-
-        tail_error_count = 5
-        tailed_errors = self.api.get_sandbox_activity(
-            sandbox_id, error_only=True, from_event_id=last_activity_id + 1, tail=tail_error_count
-        )["events"]
-        if tailed_errors:
-            self.sandbox_ended = True
-            self.teardown_errors = tailed_errors
-            self._print(f"=== Last {tail_error_count} Errors ===\n{json.dumps(tailed_errors, indent=4)}")
-
-            if raise_teardown_exception:
-                err_msg = (
-                    f"Sandbox '{name}' Error during SETUP.\n"
-                    f"Stage: '{stage}'. State '{sandbox_state}'. Sandbox ID: '{sandbox_id}'"
-                )
-                raise SandboxTeardownError(err_msg)
-
-        total_minutes = (default_timer() - start) / 60
-        self._print(f"Teardown done in {total_minutes:.2f} minutes.")
-        return sandbox_details
-
-    def rerun_setup(self):
-        self
-        pass
-
-    def run_sandbox_command_and_poll(self):
-        pass
-
-    def run_component_command_and_poll(self):
-        pass
-
-    def _get_all_activity_errors(self, sandbox_id):
-        return self.api.get_sandbox_activity(sandbox_id, error_only=True)["events"]
-
-    def publish_sandbox_id_to_env_vars(self):
-        """ publish sandbox id as environment variable for different CI process to pick up """
-        pass
-
-
-if __name__ == "__main__":
-    api = SandboxRestApiSession("localhost", "admin", "admin")
-    controller = SandboxRestController(api)
-    response = controller.start_sandbox_and_poll("rest test", "lolol")
-    print(json.dumps(response, indent=4))
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
index df7ebb0..3e4c6be 100644
--- a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
+++ b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
@@ -1,4 +1,4 @@
-Metadata-Version: 1.1
+Metadata-Version: 2.1
 Name: cloudshell-sandboxapi-wrapper
 Version: 2.0.0
 Summary: Python client for CloudShell Sandbox REST api - consume sandboxes via CI
@@ -6,36 +6,38 @@ Home-page: https://github.com/QualiSystemsLab/Sandbox-API-Python
 Author: sadanand.s
 Author-email: sadanand.s@quali.com
 License: Apache 2.0
-Description: CloudShell Sandbox API Wrapper
-        ==============================
-        
-        Installation
-        *************
-        ::
-        
-            pip install cloudshell_sandboxapi_wrapper
-        
-        Example Usage
-        **************
-        ::
-        
-            from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI
-            sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT)
-            blueprints = sandbox.get_blueprints()
-            blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id']
-            sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME)
-            sandbox.stop_sandbox(sandbox_id)
-        
-        |
-        
-        :Note:
-          Tested on cloudshell 9.3 with Python 2.7/3.7/3.8.
-          For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API <https://help.quali.com/Online%20Help/9.3/Api-Guide/Content/API/CS-Snbx-API-Topic.htm>`_
-        
-        |
 Keywords: cloudshell,sandbox,api,CI
 Platform: UNKNOWN
 Classifier: License :: OSI Approved :: Apache Software License
 Classifier: Natural Language :: English
 Classifier: Programming Language :: Python :: 3.7
 Classifier: Programming Language :: Python :: 3.8
+
+CloudShell Sandbox API Wrapper
+==============================
+
+Installation
+*************
+::
+
+    pip install cloudshell_sandboxapi_wrapper
+
+Example Usage
+**************
+::
+
+    from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI
+    sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT)
+    blueprints = sandbox.get_blueprints()
+    blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id']
+    sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME)
+    sandbox.stop_sandbox(sandbox_id)
+
+|
+
+:Note:
+  Tested on cloudshell 9.3 with Python 2.7/3.7/3.8.
+  For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API <https://help.quali.com/Online%20Help/9.3/Api-Guide/Content/API/CS-Snbx-API-Topic.htm>`_
+
+|
+
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
index 1998237..94eb6d5 100644
--- a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
+++ b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
@@ -3,10 +3,8 @@ setup.py
 cloudshell/__init__.py
 cloudshell/sandbox_rest/__init__.py
 cloudshell/sandbox_rest/sandbox_api.py
-cloudshell/sandbox_rest/sandbox_components.py
-cloudshell/sandbox_rest/sandbox_controller.py
-cloudshell/sandbox_rest/helpers/__init__.py
-cloudshell/sandbox_rest/helpers/polling_helpers.py
+cloudshell/sandbox_rest/exceptions/__init__.py
+cloudshell/sandbox_rest/exceptions/sandbox_api_exceptions.py
 cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
 cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
 cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
diff --git a/test-requirements.txt b/test-requirements.txt
index 55b033e..8c99e88 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1 +1,2 @@
-pytest
\ No newline at end of file
+pytest
+python-dotenv
\ No newline at end of file
diff --git a/tests/common.py b/tests/common.py
index 8539c15..499e09d 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1,9 +1,5 @@
 import json
 
-DUT_RESOURCE = "DUT_1"
-DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template"
-HEALTH_CHECK_COMMAND = "health_check"
-
 
 def pretty_print_response(dict_response):
     json_str = json.dumps(dict_response, indent=4)
diff --git a/tests/common_fixtures.py b/tests/common_fixtures.py
deleted file mode 100644
index 5dcb85f..0000000
--- a/tests/common_fixtures.py
+++ /dev/null
@@ -1,20 +0,0 @@
-import pytest
-
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
-
-
-@pytest.fixture
-def dut_bp_name() -> str:
-    """ For testing basic operations """
-    return "DUT Test"
-
-
-@pytest.fixture
-def dut_resource_name() -> str:
-    """ For testing basic operations """
-    return "DUT_1"
-
-
-@pytest.fixture
-def admin_session() -> SandboxRestApiSession:
-    return SandboxRestApiSession("localhost", "admin", "admin")
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..99b4aaf
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,14 @@
+import time
+
+import pytest
+from env_settings import *
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+
+
+@pytest.fixture(scope="session")
+def admin_session() -> SandboxRestApiSession:
+    with SandboxRestApiSession(CLOUDSHELL_SERVER, CLOUDSHELL_ADMIN_USER, CLOUDSHELL_ADMIN_PASSWORD) as api:
+        yield api
+        time.sleep(2)
+        print("admin session token revoked")
+
diff --git a/tests/env_settings.py b/tests/env_settings.py
new file mode 100644
index 0000000..8d05309
--- /dev/null
+++ b/tests/env_settings.py
@@ -0,0 +1,22 @@
+"""
+create .env file in tests directory with following keys:
+CLOUDSHELL_ADMIN_USER=admin
+CLOUDSHELL_ADMIN_PASSWORD=admin
+CLOUDSHELL_SERVER=localhost
+"""
+
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+# server credentials from .env
+CLOUDSHELL_ADMIN_USER = os.environ.get("CLOUDSHELL_ADMIN_USER")
+CLOUDSHELL_ADMIN_PASSWORD = os.environ.get("CLOUDSHELL_ADMIN_PASSWORD")
+CLOUDSHELL_SERVER = os.environ.get("CLOUDSHELL_SERVER")
+
+
+DUT_RESOURCE = "DUT_1"
+DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template"
+HEALTH_CHECK_COMMAND = "health_check"
+
diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py
index 6ea9528..87856bc 100644
--- a/tests/test_api _no_sandbox.py	
+++ b/tests/test_api _no_sandbox.py	
@@ -1,14 +1,12 @@
 """
-Test the api methods that do not require a live sandbox
+Functionally test the api methods that do not require a live sandbox
 - get all blueprints
 - get all sandboxes
 - get blueprint by id
 - get token + delete token
 """
-from common import DEFAULT_BLUEPRINT_TEMPLATE, pretty_print_response
-from common_fixtures import admin_session
-
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+from env_settings import DEFAULT_BLUEPRINT_TEMPLATE
 
 
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py
new file mode 100644
index 0000000..bad3cc7
--- /dev/null
+++ b/tests/test_api_empty_sandbox.py
@@ -0,0 +1,67 @@
+"""
+Test the api methods that require a live sandbox against default
+- start sandbox
+"""
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+from env_settings import DEFAULT_BLUEPRINT_TEMPLATE
+import pytest
+
+
+@pytest.fixture(scope="module")
+def sandbox_id(admin_session: SandboxRestApiSession):
+    # start sandbox
+    start_res = admin_session.start_sandbox(blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE,
+                                            sandbox_name="Pytest empty blueprint test")
+    sandbox_id = start_res["id"]
+    print(f"Sandbox started: {sandbox_id}")
+    yield sandbox_id
+    admin_session.stop_sandbox(sandbox_id)
+    print(f"\nSandbox ended: {sandbox_id}")
+
+
+def test_start_stop(admin_session, sandbox_id):
+    pass
+
+
+def test_get_sandbox_details(admin_session, sandbox_id):
+    details_res = admin_session.get_sandbox_details(sandbox_id)
+    sb_name = details_res["name"]
+    print(f"Pulled details for sandbox '{sb_name}'")
+
+
+def test_get_components(admin_session, sandbox_id):
+    sb_components = admin_session.get_sandbox_components(sandbox_id)
+    component_count = len(sb_components)
+    print(f"component count found: {component_count}")
+
+
+def test_get_sandbox_commands(admin_session, sandbox_id):
+    sb_commands = admin_session.get_sandbox_commands(sandbox_id)
+    print(f"Sandbox commands: {[x['name'] for x in sb_commands]}")
+    first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, sb_commands[0]["name"])
+    print(f"SB command name: {first_sb_command['name']}\n"
+          f"description: {first_sb_command['description']}")
+
+
+def test_get_sandbox_events(admin_session, sandbox_id):
+    activity = admin_session.get_sandbox_activity(sandbox_id)
+    events = activity["events"]
+    print(f"activity events count: {len(events)}")
+
+
+def test_get_console_output(admin_session, sandbox_id):
+    sb_output = admin_session.get_sandbox_output(sandbox_id)
+    entries = sb_output["entries"]
+    print(f"Sandbox output entries count: {len(entries)}")
+
+
+def test_get_instructions(admin_session, sandbox_id):
+    instructions = admin_session.get_sandbox_instructions(sandbox_id)
+    print(f"Pulled sandbox instructions: '{instructions}'")
+
+
+def test_extend_sandbox(admin_session, sandbox_id):
+    extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M")
+    print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}")
+
+
diff --git a/tests/test_api_with_sandbox.py b/tests/test_api_with_sandbox.py
deleted file mode 100644
index 8c1f97a..0000000
--- a/tests/test_api_with_sandbox.py
+++ /dev/null
@@ -1,33 +0,0 @@
-"""
-Test the api methods that require a live sandbox against default
-- start sandbox
-"""
-from common import DEFAULT_BLUEPRINT_TEMPLATE
-from common_fixtures import admin_session
-
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
-
-
-def test_simple_flow(admin_session: SandboxRestApiSession):
-    """ """
-    # start sandbox
-    start_res = admin_session.start_sandbox(blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_name="Pytest simple flow")
-    sandbox_id = start_res["id"]
-    assert sandbox_id
-    print(f"Sandbox started. ID: {sandbox_id}")
-
-    # get sandbox details
-    details_res = admin_session.get_sandbox_details(sandbox_id)
-    sb_name = details_res["name"]
-    print(f"Pulled details for sandbox '{sb_name}'")
-
-    # get components - should be empty
-    sb_components = admin_session.get_sandbox_components(sandbox_id)
-    component_count = len(sb_components)
-    print(f"component count found: {component_count}")
-
-    # get activity events
-
-    # get commands
-
-    #
diff --git a/tests/test_sandbox_controller.py b/tests/test_sandbox_controller.py
deleted file mode 100644
index e69de29..0000000

From 038cd3f4483703410970306ee7d8b7a2c2df0012 Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Tue, 26 Oct 2021 15:48:23 +0300
Subject: [PATCH 16/47] commiting tests

---
 .pre-commit-config.yaml                |  6 +--
 cloudshell/sandbox_rest/exceptions.py  | 10 +++--
 cloudshell/sandbox_rest/sandbox_api.py | 61 ++++++++++++--------------
 test-requirements.txt                  |  4 +-
 tests/conftest.py                      |  2 +-
 tests/env_settings.py                  |  2 +-
 tests/test_api _no_sandbox.py          |  3 +-
 tests/test_api_empty_sandbox.py        | 15 +++----
 8 files changed, 53 insertions(+), 50 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e3d0d87..d05e301 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -36,9 +36,9 @@ repos:
         language: system
         types: [python]
         args: [
-          --max-line-length=127,
-          --max-public-methods=32,
-          --max-args=8,
+#          --max-line-length=127,
+#          --max-public-methods=32,
+#          --max-args=8,
           '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding',
           '--good-names=ip,rc,eval'
         ]
\ No newline at end of file
diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py
index 634586d..f3bfa21 100644
--- a/cloudshell/sandbox_rest/exceptions.py
+++ b/cloudshell/sandbox_rest/exceptions.py
@@ -3,6 +3,7 @@
 
 class SandboxRestException(Exception):
     """ Base Exception Class inside Rest client class """
+
     def __init__(self, msg: str, response: Response = None):
         self.msg = msg
         self.response = response
@@ -18,11 +19,14 @@ def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response =
 
     @staticmethod
     def _format_response_msg(response: Response):
-        return (f"Response: {response.status_code}, Reason: {response.reason}\n"
-                f"Request URL: {response.request.url}\n"
-                f"Request Headers: {response.request.headers}")
+        return (
+            f"Response: {response.status_code}, Reason: {response.reason}\n"
+            f"Request URL: {response.request.url}\n"
+            f"Request Headers: {response.request.headers}"
+        )
 
 
 class SandboxRestAuthException(SandboxRestException):
     """ Failed auth action """
+
     pass
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 0a5cd18..9bcf004 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -1,8 +1,10 @@
 import json
 from dataclasses import asdict, dataclass
 from typing import List
+
 import requests
-from cloudshell.sandbox_rest.exceptions import SandboxRestException, SandboxRestAuthException
+
+from cloudshell.sandbox_rest.exceptions import SandboxRestAuthException, SandboxRestException
 
 
 @dataclass
@@ -22,8 +24,9 @@ class SandboxRestApiSession:
     View http://<API_SERVER>/api/v2/explore to see schemas of return json values
     """
 
-    def __init__(self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False,
-                 api_version="v2"):
+    def __init__(
+        self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, api_version="v2"
+    ):
         """ login to api and store headers for future requests """
         _protocol = "https" if is_https else "http"
         self._base_url = f"{_protocol}://{host}:{port}/api"
@@ -106,12 +109,12 @@ def delete_token(self, token_id: str) -> None:
 
     # SANDBOX POST REQUESTS
     def start_sandbox(
-            self,
-            blueprint_id: str,
-            sandbox_name="",
-            duration="PT2H0M",
-            bp_params: List[InputParam] = None,
-            permitted_users: List[str] = None,
+        self,
+        blueprint_id: str,
+        sandbox_name="",
+        duration="PT2H0M",
+        bp_params: List[InputParam] = None,
+        permitted_users: List[str] = None,
     ) -> dict:
         """
         Create a sandbox from the provided blueprint id
@@ -139,8 +142,7 @@ def start_sandbox(
         return response.json()
 
     def start_persistent_sandbox(
-            self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None,
-            permitted_users: List[str] = None
+        self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None
     ) -> dict:
         """ Create a persistent sandbox from the provided blueprint id """
         self._validate_auth_headers()
@@ -159,8 +161,9 @@ def start_persistent_sandbox(
             raise SandboxRestException(err_msg, response)
         return response.json()
 
-    def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[InputParam] = None,
-                            print_output=True) -> dict:
+    def run_sandbox_command(
+        self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True
+    ) -> dict:
         """ Run a sandbox level command """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start"
@@ -173,8 +176,7 @@ def run_sandbox_command(self, sandbox_id: str, command_name: str, params: List[I
         return response.json()
 
     def run_component_command(
-            self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None,
-            print_output: bool = True
+        self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True
     ) -> dict:
         """ Start a command on sandbox component """
         self._validate_auth_headers()
@@ -231,8 +233,9 @@ def get_sandbox_details(self, sandbox_id: str) -> dict:
             raise SandboxRestException(exc_msg, response)
         return response.json()
 
-    def get_sandbox_activity(self, sandbox_id: str, error_only=False, since="", from_event_id: int = None,
-                             tail: int = None) -> dict:
+    def get_sandbox_activity(
+        self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None
+    ) -> dict:
         """
         Get list of sandbox activity
         'since' - format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
@@ -298,9 +301,7 @@ def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> d
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get sandbox component details.\n"
-                f"component id: '{component_id}'\n"
-                f"sandbox id: '{sandbox_id}'"
+                f"Failed to get sandbox component details.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
             )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
@@ -309,14 +310,12 @@ def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) ->
         """ Get list of commands for a particular component in sandbox """
         self._validate_auth_headers()
         response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands",
-            headers=self._auth_headers
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get component commands.\n"
-                f"component id: '{component_id}'\n"
-                f"sandbox id: '{sandbox_id}'")
+                f"Failed to get component commands.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
+            )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
@@ -328,24 +327,22 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s
             headers=self._auth_headers,
         )
         if not response.ok:
-            custom_err_msg = (f"Failed to get command details.\n "
-                              f"component id: '{component_id}'\n"
-                              f"sandbox id: '{sandbox_id}'")
+            custom_err_msg = (
+                f"Failed to get command details.\n " f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
+            )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
     def get_sandbox_instructions(self, sandbox_id: str) -> str:
         """ pull the instruction text of sandbox """
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions",
-                                headers=self._auth_headers)
+        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers)
         if not response.ok:
             err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'"
             raise SandboxRestException(err_msg, response)
         return response.json()
 
-    def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None,
-                           since: str = None) -> dict:
+    def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None) -> dict:
         """ Get list of sandbox output """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}/output"
diff --git a/test-requirements.txt b/test-requirements.txt
index 8c99e88..2ed62ba 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,2 +1,4 @@
 pytest
-python-dotenv
\ No newline at end of file
+python-dotenv
+flake8
+pylint
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 99b4aaf..81052bd 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,6 +2,7 @@
 
 import pytest
 from env_settings import *
+
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
@@ -11,4 +12,3 @@ def admin_session() -> SandboxRestApiSession:
         yield api
         time.sleep(2)
         print("admin session token revoked")
-
diff --git a/tests/env_settings.py b/tests/env_settings.py
index 8d05309..51253ac 100644
--- a/tests/env_settings.py
+++ b/tests/env_settings.py
@@ -6,6 +6,7 @@
 """
 
 import os
+
 from dotenv import load_dotenv
 
 load_dotenv()
@@ -19,4 +20,3 @@
 DUT_RESOURCE = "DUT_1"
 DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template"
 HEALTH_CHECK_COMMAND = "health_check"
-
diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py
index 87856bc..7200de1 100644
--- a/tests/test_api _no_sandbox.py	
+++ b/tests/test_api _no_sandbox.py	
@@ -5,9 +5,10 @@
 - get blueprint by id
 - get token + delete token
 """
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 from env_settings import DEFAULT_BLUEPRINT_TEMPLATE
 
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+
 
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
     res = admin_session.get_sandboxes()
diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py
index bad3cc7..8b24a77 100644
--- a/tests/test_api_empty_sandbox.py
+++ b/tests/test_api_empty_sandbox.py
@@ -2,16 +2,18 @@
 Test the api methods that require a live sandbox against default
 - start sandbox
 """
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
-from env_settings import DEFAULT_BLUEPRINT_TEMPLATE
 import pytest
+from env_settings import DEFAULT_BLUEPRINT_TEMPLATE
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession):
     # start sandbox
-    start_res = admin_session.start_sandbox(blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE,
-                                            sandbox_name="Pytest empty blueprint test")
+    start_res = admin_session.start_sandbox(
+        blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_name="Pytest empty blueprint test"
+    )
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -39,8 +41,7 @@ def test_get_sandbox_commands(admin_session, sandbox_id):
     sb_commands = admin_session.get_sandbox_commands(sandbox_id)
     print(f"Sandbox commands: {[x['name'] for x in sb_commands]}")
     first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, sb_commands[0]["name"])
-    print(f"SB command name: {first_sb_command['name']}\n"
-          f"description: {first_sb_command['description']}")
+    print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}")
 
 
 def test_get_sandbox_events(admin_session, sandbox_id):
@@ -63,5 +64,3 @@ def test_get_instructions(admin_session, sandbox_id):
 def test_extend_sandbox(admin_session, sandbox_id):
     extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M")
     print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}")
-
-

From b9597024fac5830a9541ebbd3fe197718244b697 Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Thu, 28 Oct 2021 19:12:33 +0300
Subject: [PATCH 17/47] saving sample in main

---
 .gitignore                             |  3 ++-
 cloudshell/sandbox_rest/sandbox_api.py | 11 ++++++++++-
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore
index 25cf4a3..bdd9ddf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 .idea/
 .pytest_cache/
 *.egg
-tests/.env
\ No newline at end of file
+tests/.env
+*.zip
\ No newline at end of file
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 9bcf004..a50427a 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -25,7 +25,7 @@ class SandboxRestApiSession:
     """
 
     def __init__(
-        self, host: str, username: str, password="", token="", domain="Global", port=82, is_https=False, api_version="v2"
+        self, host: str, username="", password="", token="", domain="Global", port=82, is_https=False, api_version="v2"
     ):
         """ login to api and store headers for future requests """
         _protocol = "https" if is_https else "http"
@@ -398,3 +398,12 @@ def delete_execution(self, execution_id: str) -> dict:
         if not response.ok:
             raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response)
         return response.json()
+
+
+if __name__ == "__main__":
+    admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="end_users")
+    user_token = admin_api.get_token_for_target_user("end_user")
+    user_api = SandboxRestApiSession(host="localhost", token=user_token)
+    response = user_api.start_sandbox(blueprint_id="end user bp")
+    commands = user_api.get_sandbox_command_details()
+    pass
\ No newline at end of file

From e2f15dbcfe445eb49f6911aa9a22d33559f0c977 Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Thu, 28 Oct 2021 19:14:09 +0300
Subject: [PATCH 18/47] black formatted

---
 cloudshell/sandbox_rest/sandbox_api.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index a50427a..2e0e2d6 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -406,4 +406,4 @@ def delete_execution(self, execution_id: str) -> dict:
     user_api = SandboxRestApiSession(host="localhost", token=user_token)
     response = user_api.start_sandbox(blueprint_id="end user bp")
     commands = user_api.get_sandbox_command_details()
-    pass
\ No newline at end of file
+    pass

From a6c162fa8a2ec231d64924776668a514dacc7fc6 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 00:14:51 +0200
Subject: [PATCH 19/47] updated docs

---
 README.MD                                     |  20 +++++++++++++
 README.rst                                    |  27 ------------------
 .../PKG-INFO                                  |   4 +--
 .../SOURCES.txt                               |   3 +-
 ...cloudshell_sandboxapi_wrapper-2.0.0.tar.gz | Bin 5103 -> 0 bytes
 setup.py                                      |   5 ++--
 6 files changed, 26 insertions(+), 33 deletions(-)
 create mode 100644 README.MD
 delete mode 100644 README.rst
 delete mode 100644 dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz

diff --git a/README.MD b/README.MD
new file mode 100644
index 0000000..e64d887
--- /dev/null
+++ b/README.MD
@@ -0,0 +1,20 @@
+[![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python/)
+[![PyPI version](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper.svg)](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper)
+
+# Cloudshell Sandbox API Wrapper
+A python client implementation of the [cloudshell sandbox api](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/CS-Snbx-API-Topic.htm?Highlight=sandbox%20api).
+
+This client provides an object-oriented interface to instantiating an api session and interacting with the endpoints.
+All methods return a loaded python dictionary/list of the [documented](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/RefGuides/Sndbx-REST-API/REST-API-V2-Ref-Guide.htm?tocpath=CloudShell%20API%20Guide%7CCloudShell%20Sandbox%20API%7C_____3) json responses. 
+No additional library object wrapping implemented. 
+
+
+### Installation
+
+```
+pip install cloudshell_sandboxapi_wrapper
+```
+## Usage
+```python
+pass
+```
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 306cc41..0000000
--- a/README.rst
+++ /dev/null
@@ -1,27 +0,0 @@
-CloudShell Sandbox API Wrapper
-==============================
-
-Installation
-*************
-::
-
-    pip install cloudshell_sandboxapi_wrapper
-
-Example Usage
-**************
-::
-
-    from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI
-    sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT)
-    blueprints = sandbox.get_blueprints()
-    blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id']
-    sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME)
-    sandbox.stop_sandbox(sandbox_id)
-
-|
-
-:Note:
-  Tested on cloudshell 9.3 with Python 2.7/3.7/3.8.
-  For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API <https://help.quali.com/Online%20Help/9.3/Api-Guide/Content/API/CS-Snbx-API-Topic.htm>`_
-
-|
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
index 3e4c6be..6968074 100644
--- a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
+++ b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
@@ -3,8 +3,8 @@ Name: cloudshell-sandboxapi-wrapper
 Version: 2.0.0
 Summary: Python client for CloudShell Sandbox REST api - consume sandboxes via CI
 Home-page: https://github.com/QualiSystemsLab/Sandbox-API-Python
-Author: sadanand.s
-Author-email: sadanand.s@quali.com
+Author: Quali
+Author-email: support@qualisystems.com
 License: Apache 2.0
 Keywords: cloudshell,sandbox,api,CI
 Platform: UNKNOWN
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
index 94eb6d5..f956ce2 100644
--- a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
+++ b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
@@ -2,9 +2,8 @@ README.rst
 setup.py
 cloudshell/__init__.py
 cloudshell/sandbox_rest/__init__.py
+cloudshell/sandbox_rest/exceptions.py
 cloudshell/sandbox_rest/sandbox_api.py
-cloudshell/sandbox_rest/exceptions/__init__.py
-cloudshell/sandbox_rest/exceptions/sandbox_api_exceptions.py
 cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
 cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
 cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
diff --git a/dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz b/dist/cloudshell_sandboxapi_wrapper-2.0.0.tar.gz
deleted file mode 100644
index 6424e232d889ab652ddd54d37f98245b742d9a01..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/setup.py b/setup.py
index cccdeec..dfb0c59 100644
--- a/setup.py
+++ b/setup.py
@@ -18,14 +18,15 @@ def lines_from_file(file_name: str) -> List[str]:
 setup(
     name="cloudshell_sandboxapi_wrapper",
     description="Python client for CloudShell Sandbox REST api - consume sandboxes via CI",
-    keywords=["cloudshell", "sandbox", "api", "CI"],
+    keywords=["cloudshell", "sandbox", "api", "rest", "CI"],
     url="https://github.com/QualiSystemsLab/Sandbox-API-Python",
     author="sadanand.s",
     author_email="sadanand.s@quali.com",
     license="Apache 2.0",
     packages=find_packages(),
     version=read_file("version.txt"),
-    long_description=read_file("README.rst"),
+    long_description=read_file("README.MD"),
+    long_description_content_type='text/markdown',
     install_requires=lines_from_file("requirements.txt"),
     test_requires=lines_from_file("test-requirements.txt"),
     classifiers=[

From e324a776f647f968dc7f341c1ae73511fce476b8 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 00:19:05 +0200
Subject: [PATCH 20/47] updated gitignore

---
 .gitignore                                    |  4 +-
 .../PKG-INFO                                  | 43 -------------------
 .../SOURCES.txt                               | 11 -----
 .../dependency_links.txt                      |  1 -
 .../requires.txt                              |  2 -
 .../top_level.txt                             |  1 -
 6 files changed, 3 insertions(+), 59 deletions(-)
 delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
 delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
 delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
 delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/requires.txt
 delete mode 100644 cloudshell_sandboxapi_wrapper.egg-info/top_level.txt

diff --git a/.gitignore b/.gitignore
index bdd9ddf..9ae8ac3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
 .idea/
 .pytest_cache/
 *.egg
+*.egg-info
 tests/.env
-*.zip
\ No newline at end of file
+*.zip
+dist/
\ No newline at end of file
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO b/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
deleted file mode 100644
index 6968074..0000000
--- a/cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
+++ /dev/null
@@ -1,43 +0,0 @@
-Metadata-Version: 2.1
-Name: cloudshell-sandboxapi-wrapper
-Version: 2.0.0
-Summary: Python client for CloudShell Sandbox REST api - consume sandboxes via CI
-Home-page: https://github.com/QualiSystemsLab/Sandbox-API-Python
-Author: Quali
-Author-email: support@qualisystems.com
-License: Apache 2.0
-Keywords: cloudshell,sandbox,api,CI
-Platform: UNKNOWN
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Natural Language :: English
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: 3.8
-
-CloudShell Sandbox API Wrapper
-==============================
-
-Installation
-*************
-::
-
-    pip install cloudshell_sandboxapi_wrapper
-
-Example Usage
-**************
-::
-
-    from cloudshell_sandboxapi_wrapper.SandboxAPI import SandboxAPI
-    sandbox = SandboxAPI(host=SERVER_NAME, username=USERNAME, password=PASSWORD, domain=DOMAIN, port=SERVER_PORT)
-    blueprints = sandbox.get_blueprints()
-    blueprint_id = sandbox.get_blueprint_details(blueprint_id=BLUEPRINT_NAME)['id']
-    sandbox_id = sandbox.start_sandbox(BLUEPRINT_NAME, PT23H, SANDBOX_NAME)
-    sandbox.stop_sandbox(sandbox_id)
-
-|
-
-:Note:
-  Tested on cloudshell 9.3 with Python 2.7/3.7/3.8.
-  For API details, please refer to CloudShell Sandbox API help: `CloudShell Sandbox API <https://help.quali.com/Online%20Help/9.3/Api-Guide/Content/API/CS-Snbx-API-Topic.htm>`_
-
-|
-
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt b/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
deleted file mode 100644
index f956ce2..0000000
--- a/cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-README.rst
-setup.py
-cloudshell/__init__.py
-cloudshell/sandbox_rest/__init__.py
-cloudshell/sandbox_rest/exceptions.py
-cloudshell/sandbox_rest/sandbox_api.py
-cloudshell_sandboxapi_wrapper.egg-info/PKG-INFO
-cloudshell_sandboxapi_wrapper.egg-info/SOURCES.txt
-cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
-cloudshell_sandboxapi_wrapper.egg-info/requires.txt
-cloudshell_sandboxapi_wrapper.egg-info/top_level.txt
\ No newline at end of file
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt b/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
deleted file mode 100644
index 8b13789..0000000
--- a/cloudshell_sandboxapi_wrapper.egg-info/dependency_links.txt
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt b/cloudshell_sandboxapi_wrapper.egg-info/requires.txt
deleted file mode 100644
index c3e8e97..0000000
--- a/cloudshell_sandboxapi_wrapper.egg-info/requires.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-requests
-retrying
diff --git a/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt b/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt
deleted file mode 100644
index 127ff4f..0000000
--- a/cloudshell_sandboxapi_wrapper.egg-info/top_level.txt
+++ /dev/null
@@ -1 +0,0 @@
-cloudshell

From 29998c67284ed14024456f5a4a98b1f88c3e7ffe Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 14:35:26 +0200
Subject: [PATCH 21/47] all tests passing

---
 cloudshell/sandbox_rest/sandbox_api.py | 15 +++---
 tests/common.py                        |  6 +++
 tests/conftest.py                      | 18 ++++++-
 tests/constants.py                     |  7 +++
 tests/env_settings.py                  |  7 +--
 tests/test_api _no_sandbox.py          | 17 +++---
 tests/test_api_empty_sandbox.py        | 75 +++++++++++++++++++-------
 tests/test_api_heath_check_sandbox.py  | 70 ++++++++++++++++++++++++
 tests/test_rerun_setup.py              | 54 +++++++++++++++++++
 9 files changed, 230 insertions(+), 39 deletions(-)
 create mode 100644 tests/constants.py
 create mode 100644 tests/test_api_heath_check_sandbox.py
 create mode 100644 tests/test_rerun_setup.py

diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 2e0e2d6..189c9dc 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -392,18 +392,21 @@ def get_execution_details(self, execution_id: str) -> dict:
             raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response)
         return response.json()
 
-    def delete_execution(self, execution_id: str) -> dict:
+    def delete_execution(self, execution_id: str) -> None:
+        """
+        API returns dict with single key on successful deletion of execution
+        {"result": "success"}
+        """
         self._validate_auth_headers()
         response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response)
-        return response.json()
+        response_dict = response.json()
+        if not response_dict["result"] == "success":
+            raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n"
+                                       f"Response: {response_dict}")
 
 
 if __name__ == "__main__":
     admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="end_users")
     user_token = admin_api.get_token_for_target_user("end_user")
-    user_api = SandboxRestApiSession(host="localhost", token=user_token)
-    response = user_api.start_sandbox(blueprint_id="end user bp")
-    commands = user_api.get_sandbox_command_details()
-    pass
diff --git a/tests/common.py b/tests/common.py
index 499e09d..0b97e70 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1,6 +1,12 @@
 import json
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 def pretty_print_response(dict_response):
     json_str = json.dumps(dict_response, indent=4)
     print(f"\n{json_str}")
+
+
+def get_blueprint_id_from_name(api: SandboxRestApiSession, bp_name: str):
+    res = api.get_blueprint_details(bp_name)
+    return res["id"]
\ No newline at end of file
diff --git a/tests/conftest.py b/tests/conftest.py
index 81052bd..a95491e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,13 +2,29 @@
 
 import pytest
 from env_settings import *
+from constants import *
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 @pytest.fixture(scope="session")
 def admin_session() -> SandboxRestApiSession:
-    with SandboxRestApiSession(CLOUDSHELL_SERVER, CLOUDSHELL_ADMIN_USER, CLOUDSHELL_ADMIN_PASSWORD) as api:
+    with SandboxRestApiSession(host=CLOUDSHELL_SERVER,
+                               username=CLOUDSHELL_ADMIN_USER,
+                               password=CLOUDSHELL_ADMIN_PASSWORD,
+                               domain=CLOUDSHELL_DOMAIN) as api:
         yield api
         time.sleep(2)
         print("admin session token revoked")
+
+
+@pytest.fixture(scope="session")
+def empty_blueprint():
+    return DEFAULT_EMPTY_BLUEPRINT
+
+
+@pytest.fixture(scope="session")
+def dut_blueprint():
+    return DUT_BLUEPRINT
+
+
diff --git a/tests/constants.py b/tests/constants.py
new file mode 100644
index 0000000..77becad
--- /dev/null
+++ b/tests/constants.py
@@ -0,0 +1,7 @@
+DEFAULT_EMPTY_BLUEPRINT = "CloudShell Sandbox Template"
+BLUEPRINT_SETUP_COMMAND = "Setup"
+
+# DUT blueprint constants
+DUT_MODEL = "Putshell"
+DUT_COMMAND = "health_check"
+DUT_BLUEPRINT = "DUT Blueprint Test"
diff --git a/tests/env_settings.py b/tests/env_settings.py
index 51253ac..306daa0 100644
--- a/tests/env_settings.py
+++ b/tests/env_settings.py
@@ -3,6 +3,7 @@
 CLOUDSHELL_ADMIN_USER=admin
 CLOUDSHELL_ADMIN_PASSWORD=admin
 CLOUDSHELL_SERVER=localhost
+CLOUDSHELL_DOMAIN=Global
 """
 
 import os
@@ -15,8 +16,4 @@
 CLOUDSHELL_ADMIN_USER = os.environ.get("CLOUDSHELL_ADMIN_USER")
 CLOUDSHELL_ADMIN_PASSWORD = os.environ.get("CLOUDSHELL_ADMIN_PASSWORD")
 CLOUDSHELL_SERVER = os.environ.get("CLOUDSHELL_SERVER")
-
-
-DUT_RESOURCE = "DUT_1"
-DEFAULT_BLUEPRINT_TEMPLATE = "CloudShell Sandbox Template"
-HEALTH_CHECK_COMMAND = "health_check"
+CLOUDSHELL_DOMAIN = os.environ.get("CLOUDSHELL_DOMAIN")
diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py
index 7200de1..f95d6b5 100644
--- a/tests/test_api _no_sandbox.py	
+++ b/tests/test_api _no_sandbox.py	
@@ -1,27 +1,27 @@
 """
-Functionally test the api methods that do not require a live sandbox
-- get all blueprints
-- get all sandboxes
-- get blueprint by id
-- get token + delete token
+Test the api methods that do NOT require a blueprint
+Live Cloudshell server is still a dependency
 """
-from env_settings import DEFAULT_BLUEPRINT_TEMPLATE
+from constants import DEFAULT_EMPTY_BLUEPRINT
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
     res = admin_session.get_sandboxes()
+    assert(type(res) is list)
     print(f"Sandbox count found in system: {len(res)}")
 
 
 def test_get_blueprints(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprints()
+    assert(type(bp_res) is list)
     print(f"Blueprint count found in system: '{len(bp_res)}'")
 
 
 def test_get_default_blueprint(admin_session: SandboxRestApiSession):
-    bp_res = admin_session.get_blueprint_details(DEFAULT_BLUEPRINT_TEMPLATE)
+    bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT)
+    assert(type(bp_res) is dict)
     bp_name = bp_res["name"]
     print(f"Pulled details for '{bp_name}'")
 
@@ -29,6 +29,5 @@ def test_get_default_blueprint(admin_session: SandboxRestApiSession):
 def test_get_and_delete_token(admin_session: SandboxRestApiSession):
     """ get token for admin user """
     token_res = admin_session.get_token_for_target_user("admin")
+    assert(type(token_res) is str)
     print(f"Token response: '{token_res}'")
-    admin_session.delete_token(token_res)
-    print("deleted token")
diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py
index 8b24a77..93edde5 100644
--- a/tests/test_api_empty_sandbox.py
+++ b/tests/test_api_empty_sandbox.py
@@ -1,18 +1,26 @@
 """
-Test the api methods that require a live sandbox against default
-- start sandbox
+Test the api methods that require an empty, PUBLIC blueprint
 """
-import pytest
-from env_settings import DEFAULT_BLUEPRINT_TEMPLATE
+import time
 
+import pytest
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+from constants import *
+from common import *
+
+
+@pytest.fixture(scope="module")
+def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint):
+    res_id = get_blueprint_id_from_name(admin_session, empty_blueprint)
+    assert(type(res_id) is str)
+    return res_id
 
 
 @pytest.fixture(scope="module")
-def sandbox_id(admin_session: SandboxRestApiSession):
+def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     # start sandbox
     start_res = admin_session.start_sandbox(
-        blueprint_id=DEFAULT_BLUEPRINT_TEMPLATE, sandbox_name="Pytest empty blueprint test"
+        blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test"
     )
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
@@ -21,46 +29,77 @@ def sandbox_id(admin_session: SandboxRestApiSession):
     print(f"\nSandbox ended: {sandbox_id}")
 
 
+@pytest.fixture(scope="module")
+def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
+    polling_minutes = 2
+    counter = 0
+    while True:
+        if counter > polling_minutes:
+            raise Exception("Timeout waiting for setup to end")
+        state = admin_session.get_sandbox_details(sandbox_id)["state"]
+        if state == "Ready":
+            break
+        time.sleep(60)
+        counter += 1
+
+    print("Rerunning Setup...")
+    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id,
+                                            command_name=BLUEPRINT_SETUP_COMMAND)
+    assert (type(res) is dict)
+    print("Setup re-run execution response")
+    pretty_print_response(res)
+    execution_id = res["executionId"]
+    return execution_id
+
+
 def test_start_stop(admin_session, sandbox_id):
-    pass
+    assert (type(sandbox_id) is str)
+    print(f"Sandbox ID: {sandbox_id}")
 
 
 def test_get_sandbox_details(admin_session, sandbox_id):
     details_res = admin_session.get_sandbox_details(sandbox_id)
+    assert (type(details_res) is dict)
     sb_name = details_res["name"]
     print(f"Pulled details for sandbox '{sb_name}'")
 
 
 def test_get_components(admin_session, sandbox_id):
-    sb_components = admin_session.get_sandbox_components(sandbox_id)
-    component_count = len(sb_components)
+    components_res = admin_session.get_sandbox_components(sandbox_id)
+    assert (type(components_res) is list)
+    component_count = len(components_res)
     print(f"component count found: {component_count}")
 
 
 def test_get_sandbox_commands(admin_session, sandbox_id):
-    sb_commands = admin_session.get_sandbox_commands(sandbox_id)
-    print(f"Sandbox commands: {[x['name'] for x in sb_commands]}")
-    first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, sb_commands[0]["name"])
+    commands_res = admin_session.get_sandbox_commands(sandbox_id)
+    assert (type(commands_res) is list)
+    print(f"Sandbox commands: {[x['name'] for x in commands_res]}")
+    first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, commands_res[0]["name"])
     print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}")
 
 
 def test_get_sandbox_events(admin_session, sandbox_id):
-    activity = admin_session.get_sandbox_activity(sandbox_id)
-    events = activity["events"]
+    activity_res = admin_session.get_sandbox_activity(sandbox_id)
+    assert (type(activity_res) is dict and "events" in activity_res)
+    events = activity_res["events"]
     print(f"activity events count: {len(events)}")
 
 
 def test_get_console_output(admin_session, sandbox_id):
-    sb_output = admin_session.get_sandbox_output(sandbox_id)
-    entries = sb_output["entries"]
+    output_res = admin_session.get_sandbox_output(sandbox_id)
+    assert (type(output_res) is dict and "entries" in output_res)
+    entries = output_res["entries"]
     print(f"Sandbox output entries count: {len(entries)}")
 
 
 def test_get_instructions(admin_session, sandbox_id):
-    instructions = admin_session.get_sandbox_instructions(sandbox_id)
-    print(f"Pulled sandbox instructions: '{instructions}'")
+    instructions_res = admin_session.get_sandbox_instructions(sandbox_id)
+    assert (type(instructions_res) is str)
+    print(f"Pulled sandbox instructions: '{instructions_res}'")
 
 
 def test_extend_sandbox(admin_session, sandbox_id):
     extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M")
+    assert (type(extend_response) is dict and "remaining_time" in extend_response)
     print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}")
diff --git a/tests/test_api_heath_check_sandbox.py b/tests/test_api_heath_check_sandbox.py
new file mode 100644
index 0000000..25a47fc
--- /dev/null
+++ b/tests/test_api_heath_check_sandbox.py
@@ -0,0 +1,70 @@
+"""
+Test the api methods against blueprint with a resource containing a command
+- Putshell mock can be used - https://community.quali.com/repos/3318/put-shell-mock
+- DUT model / command can be referenced in constants.py (Putshell / health_check)
+- Assumed that only one DUT is in blueprint
+"""
+import time
+
+import pytest
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+from constants import *
+from common import *
+
+
+@pytest.fixture(scope="module")
+def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint):
+    res_id = get_blueprint_id_from_name(admin_session, dut_blueprint)
+    assert(type(res_id) is str)
+    return res_id
+
+
+@pytest.fixture(scope="module")
+def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
+    # start sandbox
+    start_res = admin_session.start_sandbox(
+        blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test"
+    )
+    sandbox_id = start_res["id"]
+    print(f"Sandbox started: {sandbox_id}")
+    yield sandbox_id
+    admin_session.stop_sandbox(sandbox_id)
+    print(f"\nSandbox ended: {sandbox_id}")
+
+
+@pytest.fixture(scope="module")
+def component_id(admin_session: SandboxRestApiSession, sandbox_id: str, dut_blueprint: str):
+    components = admin_session.get_sandbox_components(sandbox_id)
+    component_filter = [x for x in components if x["component_type"] == DUT_MODEL]
+    assert component_filter
+    return component_filter[0]["id"]
+
+
+@pytest.fixture(scope="module")
+def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str):
+    print("Starting DUT Command...")
+    res = admin_session.run_component_command(sandbox_id=sandbox_id,
+                                              component_id=component_id,
+                                              command_name=DUT_COMMAND)
+    assert(type(res) is dict)
+    print("Started execution response")
+    pretty_print_response(res)
+    execution_id = res["executionId"]
+    return execution_id
+
+
+@pytest.fixture(scope="module")
+def test_get_execution_details(admin_session, execution_id):
+    res = admin_session.get_execution_details(execution_id)
+    assert(type(res) is dict)
+    print("Execution Details")
+    pretty_print_response(res)
+
+
+def test_delete_execution(admin_session, execution_id, test_get_execution_details):
+    print("Stopping execution...")
+    time.sleep(1)
+    admin_session.delete_execution(execution_id)
+    print("Execution deleted")
+
+
diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py
new file mode 100644
index 0000000..8827729
--- /dev/null
+++ b/tests/test_rerun_setup.py
@@ -0,0 +1,54 @@
+"""
+Test re-running setup to validate blueprint level commands, getting details, and then ending setup execution, finally end sandbox
+"""
+import time
+
+import pytest
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+from constants import *
+from common import *
+
+
+@pytest.fixture(scope="module")
+def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint):
+    # start sandbox
+    start_res = admin_session.start_sandbox(
+        blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test"
+    )
+    sandbox_id = start_res["id"]
+    print(f"Sandbox started: {sandbox_id}")
+    yield sandbox_id
+    admin_session.stop_sandbox(sandbox_id)
+    print(f"\nSandbox ended: {sandbox_id}")
+
+
+@pytest.fixture(scope="module")
+def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
+    polling_minutes = 2
+    counter = 0
+    while True:
+        if counter > polling_minutes:
+            raise Exception("Timeout waiting for setup to end")
+        state = admin_session.get_sandbox_details(sandbox_id)["state"]
+        if state == "Ready":
+            break
+        time.sleep(60)
+        counter += 1
+
+    print("Rerunning Setup...")
+    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id,
+                                            command_name=BLUEPRINT_SETUP_COMMAND)
+    assert (type(res) is dict)
+    print("Setup re-run execution response")
+    pretty_print_response(res)
+    execution_id = res["executionId"]
+    return execution_id
+
+
+def test_cancel_setup(admin_session, setup_execution_id):
+    print("Setup Execution Details")
+    res = admin_session.get_execution_details(setup_execution_id)
+    pretty_print_response(res)
+    print("Ending setup execution")
+    admin_session.delete_execution(setup_execution_id)
+    print("Setup execution cancelled")

From ea5681340da2496c66391480cdd67656ab7c0028 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 14:35:48 +0200
Subject: [PATCH 22/47] black reformatted

---
 cloudshell/sandbox_rest/sandbox_api.py |  3 +--
 tests/common.py                        |  3 ++-
 tests/conftest.py                      | 11 ++++-----
 tests/test_api _no_sandbox.py          |  8 +++----
 tests/test_api_empty_sandbox.py        | 32 ++++++++++++--------------
 tests/test_api_heath_check_sandbox.py  | 21 +++++++----------
 tests/test_rerun_setup.py              | 14 +++++------
 7 files changed, 40 insertions(+), 52 deletions(-)

diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 189c9dc..fc5e333 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -403,8 +403,7 @@ def delete_execution(self, execution_id: str) -> None:
             raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response)
         response_dict = response.json()
         if not response_dict["result"] == "success":
-            raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n"
-                                       f"Response: {response_dict}")
+            raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}")
 
 
 if __name__ == "__main__":
diff --git a/tests/common.py b/tests/common.py
index 0b97e70..962b5ce 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1,4 +1,5 @@
 import json
+
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
@@ -9,4 +10,4 @@ def pretty_print_response(dict_response):
 
 def get_blueprint_id_from_name(api: SandboxRestApiSession, bp_name: str):
     res = api.get_blueprint_details(bp_name)
-    return res["id"]
\ No newline at end of file
+    return res["id"]
diff --git a/tests/conftest.py b/tests/conftest.py
index a95491e..dadf93a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,18 +1,17 @@
 import time
 
 import pytest
-from env_settings import *
 from constants import *
+from env_settings import *
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 @pytest.fixture(scope="session")
 def admin_session() -> SandboxRestApiSession:
-    with SandboxRestApiSession(host=CLOUDSHELL_SERVER,
-                               username=CLOUDSHELL_ADMIN_USER,
-                               password=CLOUDSHELL_ADMIN_PASSWORD,
-                               domain=CLOUDSHELL_DOMAIN) as api:
+    with SandboxRestApiSession(
+        host=CLOUDSHELL_SERVER, username=CLOUDSHELL_ADMIN_USER, password=CLOUDSHELL_ADMIN_PASSWORD, domain=CLOUDSHELL_DOMAIN
+    ) as api:
         yield api
         time.sleep(2)
         print("admin session token revoked")
@@ -26,5 +25,3 @@ def empty_blueprint():
 @pytest.fixture(scope="session")
 def dut_blueprint():
     return DUT_BLUEPRINT
-
-
diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py
index f95d6b5..4131083 100644
--- a/tests/test_api _no_sandbox.py	
+++ b/tests/test_api _no_sandbox.py	
@@ -9,19 +9,19 @@
 
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
     res = admin_session.get_sandboxes()
-    assert(type(res) is list)
+    assert type(res) is list
     print(f"Sandbox count found in system: {len(res)}")
 
 
 def test_get_blueprints(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprints()
-    assert(type(bp_res) is list)
+    assert type(bp_res) is list
     print(f"Blueprint count found in system: '{len(bp_res)}'")
 
 
 def test_get_default_blueprint(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT)
-    assert(type(bp_res) is dict)
+    assert type(bp_res) is dict
     bp_name = bp_res["name"]
     print(f"Pulled details for '{bp_name}'")
 
@@ -29,5 +29,5 @@ def test_get_default_blueprint(admin_session: SandboxRestApiSession):
 def test_get_and_delete_token(admin_session: SandboxRestApiSession):
     """ get token for admin user """
     token_res = admin_session.get_token_for_target_user("admin")
-    assert(type(token_res) is str)
+    assert type(token_res) is str
     print(f"Token response: '{token_res}'")
diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py
index 93edde5..b490bba 100644
--- a/tests/test_api_empty_sandbox.py
+++ b/tests/test_api_empty_sandbox.py
@@ -4,24 +4,23 @@
 import time
 
 import pytest
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
-from constants import *
 from common import *
+from constants import *
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 @pytest.fixture(scope="module")
 def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint):
     res_id = get_blueprint_id_from_name(admin_session, empty_blueprint)
-    assert(type(res_id) is str)
+    assert type(res_id) is str
     return res_id
 
 
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     # start sandbox
-    start_res = admin_session.start_sandbox(
-        blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test"
-    )
+    start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test")
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -43,9 +42,8 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
         counter += 1
 
     print("Rerunning Setup...")
-    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id,
-                                            command_name=BLUEPRINT_SETUP_COMMAND)
-    assert (type(res) is dict)
+    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND)
+    assert type(res) is dict
     print("Setup re-run execution response")
     pretty_print_response(res)
     execution_id = res["executionId"]
@@ -53,27 +51,27 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
 
 
 def test_start_stop(admin_session, sandbox_id):
-    assert (type(sandbox_id) is str)
+    assert type(sandbox_id) is str
     print(f"Sandbox ID: {sandbox_id}")
 
 
 def test_get_sandbox_details(admin_session, sandbox_id):
     details_res = admin_session.get_sandbox_details(sandbox_id)
-    assert (type(details_res) is dict)
+    assert type(details_res) is dict
     sb_name = details_res["name"]
     print(f"Pulled details for sandbox '{sb_name}'")
 
 
 def test_get_components(admin_session, sandbox_id):
     components_res = admin_session.get_sandbox_components(sandbox_id)
-    assert (type(components_res) is list)
+    assert type(components_res) is list
     component_count = len(components_res)
     print(f"component count found: {component_count}")
 
 
 def test_get_sandbox_commands(admin_session, sandbox_id):
     commands_res = admin_session.get_sandbox_commands(sandbox_id)
-    assert (type(commands_res) is list)
+    assert type(commands_res) is list
     print(f"Sandbox commands: {[x['name'] for x in commands_res]}")
     first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, commands_res[0]["name"])
     print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}")
@@ -81,25 +79,25 @@ def test_get_sandbox_commands(admin_session, sandbox_id):
 
 def test_get_sandbox_events(admin_session, sandbox_id):
     activity_res = admin_session.get_sandbox_activity(sandbox_id)
-    assert (type(activity_res) is dict and "events" in activity_res)
+    assert type(activity_res) is dict and "events" in activity_res
     events = activity_res["events"]
     print(f"activity events count: {len(events)}")
 
 
 def test_get_console_output(admin_session, sandbox_id):
     output_res = admin_session.get_sandbox_output(sandbox_id)
-    assert (type(output_res) is dict and "entries" in output_res)
+    assert type(output_res) is dict and "entries" in output_res
     entries = output_res["entries"]
     print(f"Sandbox output entries count: {len(entries)}")
 
 
 def test_get_instructions(admin_session, sandbox_id):
     instructions_res = admin_session.get_sandbox_instructions(sandbox_id)
-    assert (type(instructions_res) is str)
+    assert type(instructions_res) is str
     print(f"Pulled sandbox instructions: '{instructions_res}'")
 
 
 def test_extend_sandbox(admin_session, sandbox_id):
     extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M")
-    assert (type(extend_response) is dict and "remaining_time" in extend_response)
+    assert type(extend_response) is dict and "remaining_time" in extend_response
     print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}")
diff --git a/tests/test_api_heath_check_sandbox.py b/tests/test_api_heath_check_sandbox.py
index 25a47fc..43bded9 100644
--- a/tests/test_api_heath_check_sandbox.py
+++ b/tests/test_api_heath_check_sandbox.py
@@ -7,24 +7,23 @@
 import time
 
 import pytest
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
-from constants import *
 from common import *
+from constants import *
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 @pytest.fixture(scope="module")
 def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint):
     res_id = get_blueprint_id_from_name(admin_session, dut_blueprint)
-    assert(type(res_id) is str)
+    assert type(res_id) is str
     return res_id
 
 
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     # start sandbox
-    start_res = admin_session.start_sandbox(
-        blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test"
-    )
+    start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test")
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -43,10 +42,8 @@ def component_id(admin_session: SandboxRestApiSession, sandbox_id: str, dut_blue
 @pytest.fixture(scope="module")
 def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str):
     print("Starting DUT Command...")
-    res = admin_session.run_component_command(sandbox_id=sandbox_id,
-                                              component_id=component_id,
-                                              command_name=DUT_COMMAND)
-    assert(type(res) is dict)
+    res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND)
+    assert type(res) is dict
     print("Started execution response")
     pretty_print_response(res)
     execution_id = res["executionId"]
@@ -56,7 +53,7 @@ def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, componen
 @pytest.fixture(scope="module")
 def test_get_execution_details(admin_session, execution_id):
     res = admin_session.get_execution_details(execution_id)
-    assert(type(res) is dict)
+    assert type(res) is dict
     print("Execution Details")
     pretty_print_response(res)
 
@@ -66,5 +63,3 @@ def test_delete_execution(admin_session, execution_id, test_get_execution_detail
     time.sleep(1)
     admin_session.delete_execution(execution_id)
     print("Execution deleted")
-
-
diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py
index 8827729..12da642 100644
--- a/tests/test_rerun_setup.py
+++ b/tests/test_rerun_setup.py
@@ -4,17 +4,16 @@
 import time
 
 import pytest
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
-from constants import *
 from common import *
+from constants import *
+
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint):
     # start sandbox
-    start_res = admin_session.start_sandbox(
-        blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test"
-    )
+    start_res = admin_session.start_sandbox(blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test")
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -36,9 +35,8 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
         counter += 1
 
     print("Rerunning Setup...")
-    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id,
-                                            command_name=BLUEPRINT_SETUP_COMMAND)
-    assert (type(res) is dict)
+    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND)
+    assert type(res) is dict
     print("Setup re-run execution response")
     pretty_print_response(res)
     execution_id = res["executionId"]

From 94a53f237fbe23fdb2e62c4191b1565a99af3c00 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 14:38:30 +0200
Subject: [PATCH 23/47] isort fix

---
 cloudshell/sandbox_rest/sandbox_api.py | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index fc5e333..35c7484 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -4,7 +4,8 @@
 
 import requests
 
-from cloudshell.sandbox_rest.exceptions import SandboxRestAuthException, SandboxRestException
+from cloudshell.sandbox_rest.exceptions import (SandboxRestAuthException,
+                                                SandboxRestException)
 
 
 @dataclass
@@ -404,8 +405,3 @@ def delete_execution(self, execution_id: str) -> None:
         response_dict = response.json()
         if not response_dict["result"] == "success":
             raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}")
-
-
-if __name__ == "__main__":
-    admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="end_users")
-    user_token = admin_api.get_token_for_target_user("end_user")

From c386fcdfb238405a106f93f3b499382d78247b55 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 14:42:20 +0200
Subject: [PATCH 24/47] black formatted

---
 cloudshell/sandbox_rest/exceptions.py  |   8 +-
 cloudshell/sandbox_rest/sandbox_api.py | 268 ++++++++++++++++++-------
 setup.py                               |   2 +-
 tests/conftest.py                      |   5 +-
 tests/test_api _no_sandbox.py          |   2 +-
 tests/test_api_empty_sandbox.py        |  17 +-
 tests/test_api_heath_check_sandbox.py  |  16 +-
 tests/test_rerun_setup.py              |   8 +-
 8 files changed, 242 insertions(+), 84 deletions(-)

diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py
index f3bfa21..c4154ff 100644
--- a/cloudshell/sandbox_rest/exceptions.py
+++ b/cloudshell/sandbox_rest/exceptions.py
@@ -2,7 +2,7 @@
 
 
 class SandboxRestException(Exception):
-    """ Base Exception Class inside Rest client class """
+    """Base Exception Class inside Rest client class"""
 
     def __init__(self, msg: str, response: Response = None):
         self.msg = msg
@@ -11,7 +11,9 @@ def __init__(self, msg: str, response: Response = None):
     def __str__(self):
         return self._format_err_msg(self.msg, self.response)
 
-    def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = None) -> str:
+    def _format_err_msg(
+        self, custom_err_msg="Failed Api Call", response: Response = None
+    ) -> str:
         err_msg = f"Sandbox API Error: {custom_err_msg}"
 
         if response:
@@ -27,6 +29,6 @@ def _format_response_msg(response: Response):
 
 
 class SandboxRestAuthException(SandboxRestException):
-    """ Failed auth action """
+    """Failed auth action"""
 
     pass
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 35c7484..c673ad0 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -4,8 +4,10 @@
 
 import requests
 
-from cloudshell.sandbox_rest.exceptions import (SandboxRestAuthException,
-                                                SandboxRestException)
+from cloudshell.sandbox_rest.exceptions import (
+    SandboxRestAuthException,
+    SandboxRestException,
+)
 
 
 @dataclass
@@ -26,9 +28,17 @@ class SandboxRestApiSession:
     """
 
     def __init__(
-        self, host: str, username="", password="", token="", domain="Global", port=82, is_https=False, api_version="v2"
+        self,
+        host: str,
+        username="",
+        password="",
+        token="",
+        domain="Global",
+        port=82,
+        is_https=False,
+        api_version="v2",
     ):
-        """ login to api and store headers for future requests """
+        """login to api and store headers for future requests"""
         _protocol = "https" if is_https else "http"
         self._base_url = f"{_protocol}://{host}:{port}/api"
         self._versioned_url = f"{self._base_url}/{api_version}"
@@ -37,7 +47,9 @@ def __init__(
         self._password = password
         self.domain = domain
         if self.username and self._password:
-            self.auth_token = self.get_token_with_credentials(username, password, domain)
+            self.auth_token = self.get_token_with_credentials(
+                username, password, domain
+            )
         elif token:
             self.auth_token = token
         self._auth_headers = self._build_auth_headers(self.auth_token)
@@ -55,11 +67,16 @@ def _build_auth_headers(login_token: str) -> dict:
         """
         interpolate token into auth_headers dict
         """
-        auth_headers = {"Authorization": f"Basic {login_token}", "Content-Type": "application/json"}
+        auth_headers = {
+            "Authorization": f"Basic {login_token}",
+            "Content-Type": "application/json",
+        }
         return auth_headers
 
     def refresh_auth_from_stored_credentials(self) -> None:
-        self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain)
+        self.auth_token = self.get_token_with_credentials(
+            self.username, self._password, self.domain
+        )
         self._auth_headers = self._build_auth_headers(self.auth_token)
 
     def invalidate_auth(self) -> None:
@@ -68,16 +85,22 @@ def invalidate_auth(self) -> None:
 
     def _validate_auth_headers(self) -> None:
         if not self._auth_headers:
-            raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session")
+            raise SandboxRestAuthException(
+                f"No auth headers currently set for '{self.username}' session"
+            )
 
     # LOGIN METHODS
-    def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str:
+    def get_token_with_credentials(
+        self, user_name: str, password: str, domain: str
+    ) -> str:
         """
         Get token from credentials - extraneous quotes stripped off token string
         """
         response = requests.put(
             url=f"{self._base_url}/login",
-            data=json.dumps({"username": user_name, "password": password, "domain": domain}),
+            data=json.dumps(
+                {"username": user_name, "password": password, "domain": domain}
+            ),
             headers={"Content-Type": "application/json"},
         )
 
@@ -93,18 +116,24 @@ def get_token_for_target_user(self, user_name: str) -> str:
         """
         self._validate_auth_headers()
         response = requests.post(
-            url=f"{self._base_url}/token", data=json.dumps({"username": user_name}), headers=self._auth_headers
+            url=f"{self._base_url}/token",
+            data=json.dumps({"username": user_name}),
+            headers=self._auth_headers,
         )
 
         if not response.ok:
-            raise SandboxRestException(f"Failed to get get token for user {user_name}", response)
+            raise SandboxRestException(
+                f"Failed to get get token for user {user_name}", response
+            )
 
         login_token = response.text[1:-1]
         return login_token
 
     def delete_token(self, token_id: str) -> None:
         self._validate_auth_headers()
-        response = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers)
+        response = requests.delete(
+            url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers
+        )
         if not response.ok:
             raise SandboxRestException("Failed to delete token", response)
 
@@ -123,7 +152,11 @@ def start_sandbox(
         """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/blueprints/{blueprint_id}/start"
-        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
+        sandbox_name = (
+            sandbox_name
+            if sandbox_name
+            else self.get_blueprint_details(blueprint_id)["name"]
+        )
 
         data_dict = {
             "duration": duration,
@@ -132,7 +165,9 @@ def start_sandbox(
             "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
-        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
+        response = requests.post(
+            url, headers=self._auth_headers, data=json.dumps(data_dict)
+        )
         if not response.ok:
             custom_msg = f"Failed to start '{blueprint_id}'. "
             if 400 <= response.status_code < 500:
@@ -143,51 +178,80 @@ def start_sandbox(
         return response.json()
 
     def start_persistent_sandbox(
-        self, blueprint_id: str, sandbox_name="", bp_params: List[InputParam] = None, permitted_users: List[str] = None
+        self,
+        blueprint_id: str,
+        sandbox_name="",
+        bp_params: List[InputParam] = None,
+        permitted_users: List[str] = None,
     ) -> dict:
-        """ Create a persistent sandbox from the provided blueprint id """
+        """Create a persistent sandbox from the provided blueprint id"""
         self._validate_auth_headers()
         url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent"
 
-        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
+        sandbox_name = (
+            sandbox_name
+            if sandbox_name
+            else self.get_blueprint_details(blueprint_id)["name"]
+        )
         data_dict = {
             "name": sandbox_name,
             "permitted_users": permitted_users if permitted_users else [],
             "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
-        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
+        response = requests.post(
+            url, headers=self._auth_headers, data=json.dumps(data_dict)
+        )
         if not response.ok:
-            err_msg = f"Failed to start persistent sandbox from blueprint '{blueprint_id}'"
+            err_msg = (
+                f"Failed to start persistent sandbox from blueprint '{blueprint_id}'"
+            )
             raise SandboxRestException(err_msg, response)
         return response.json()
 
     def run_sandbox_command(
-        self, sandbox_id: str, command_name: str, params: List[InputParam] = None, print_output=True
+        self,
+        sandbox_id: str,
+        command_name: str,
+        params: List[InputParam] = None,
+        print_output=True,
     ) -> dict:
-        """ Run a sandbox level command """
+        """Run a sandbox level command"""
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start"
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
-        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
+        response = requests.post(
+            url, data=json.dumps(data_dict), headers=self._auth_headers
+        )
         if not response.ok:
-            raise SandboxRestException(f"failed to start sandbox command '{command_name}'", response)
+            raise SandboxRestException(
+                f"failed to start sandbox command '{command_name}'", response
+            )
         return response.json()
 
     def run_component_command(
-        self, sandbox_id: str, component_id: str, command_name: str, params: List[InputParam] = None, print_output: bool = True
+        self,
+        sandbox_id: str,
+        component_id: str,
+        command_name: str,
+        params: List[InputParam] = None,
+        print_output: bool = True,
     ) -> dict:
-        """ Start a command on sandbox component """
+        """Start a command on sandbox component"""
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start"
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
-        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
+        response = requests.post(
+            url, data=json.dumps(data_dict), headers=self._auth_headers
+        )
         if not response.ok:
-            raise SandboxRestException(f"failed to start component command '{command_name}'", response)
+            raise SandboxRestException(
+                f"failed to start component command '{command_name}'", response
+            )
         return response.json()
 
     def extend_sandbox(self, sandbox_id: str, duration: str) -> dict:
@@ -199,22 +263,31 @@ def extend_sandbox(self, sandbox_id: str, duration: str) -> dict:
         self._validate_auth_headers()
         data_dict = {"extended_time": duration}
         response = requests.post(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/extend", data=json.dumps(data_dict), headers=self._auth_headers
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/extend",
+            data=json.dumps(data_dict),
+            headers=self._auth_headers,
         )
         if not response.ok:
-            raise SandboxRestException(f"failed to extend sandbox '{sandbox_id}'", response)
+            raise SandboxRestException(
+                f"failed to extend sandbox '{sandbox_id}'", response
+            )
         return response.json()
 
     def stop_sandbox(self, sandbox_id: str) -> None:
-        """ Stop the sandbox given sandbox id """
+        """Stop the sandbox given sandbox id"""
         self._validate_auth_headers()
-        response = requests.post(f"{self._versioned_url}/sandboxes/{sandbox_id}/stop", headers=self._auth_headers)
+        response = requests.post(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/stop",
+            headers=self._auth_headers,
+        )
         if not response.ok:
-            raise SandboxRestException(f"Failed to stop sandbox '{sandbox_id}'", response)
+            raise SandboxRestException(
+                f"Failed to stop sandbox '{sandbox_id}'", response
+            )
 
     # SANDBOX GET REQUESTS
     def get_sandboxes(self, show_historic=False) -> list:
-        """ Get list of sandboxes """
+        """Get list of sandboxes"""
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes"
         params = {"show_historic": "true" if show_historic else "false"}
@@ -225,7 +298,7 @@ def get_sandboxes(self, show_historic=False) -> list:
         return response.json()
 
     def get_sandbox_details(self, sandbox_id: str) -> dict:
-        """ Get details of the given sandbox id """
+        """Get details of the given sandbox id"""
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}"
         response = requests.get(url, headers=self._auth_headers)
@@ -235,7 +308,12 @@ def get_sandbox_details(self, sandbox_id: str) -> dict:
         return response.json()
 
     def get_sandbox_activity(
-        self, sandbox_id: str, error_only=False, since="", from_event_id: int = None, tail: int = None
+        self,
+        sandbox_id: str,
+        error_only=False,
+        since="",
+        from_event_id: int = None,
+        tail: int = None,
     ) -> dict:
         """
         Get list of sandbox activity
@@ -263,65 +341,88 @@ def get_sandbox_activity(
             response = requests.get(url, headers=self._auth_headers)
 
         if not response.ok:
-            raise SandboxRestException(f"Failed to get sandbox activity for '{sandbox_id}'", response)
+            raise SandboxRestException(
+                f"Failed to get sandbox activity for '{sandbox_id}'", response
+            )
         return response.json()
 
     def get_sandbox_commands(self, sandbox_id: str) -> list:
-        """ Get list of sandbox commands """
+        """Get list of sandbox commands"""
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/commands", headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/commands",
+            headers=self._auth_headers,
+        )
         if not response.ok:
-            raise SandboxRestException(f"Failed to get sandbox commands for '{sandbox_id}'", response)
+            raise SandboxRestException(
+                f"Failed to get sandbox commands for '{sandbox_id}'", response
+            )
         return response.json()
 
     def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dict:
-        """ Get details of specific sandbox command """
+        """Get details of specific sandbox command"""
         self._validate_auth_headers()
         response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}", headers=self._auth_headers
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}",
+            headers=self._auth_headers,
         )
         if not response.ok:
-            err_msg = f"Failed to get command details for '{command_name}' in '{sandbox_id}'"
+            err_msg = (
+                f"Failed to get command details for '{command_name}' in '{sandbox_id}'"
+            )
             raise SandboxRestException(err_msg, response)
         return response.json()
 
     def get_sandbox_components(self, sandbox_id: str) -> list:
-        """ Get list of sandbox components """
+        """Get list of sandbox components"""
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/components", headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components",
+            headers=self._auth_headers,
+        )
         if not response.ok:
             err_msg = (response, f"Failed to get components for '{sandbox_id}'")
             raise SandboxRestException(err_msg, response)
         return response.json()
 
     def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> dict:
-        """ Get details of components in sandbox """
+        """Get details of components in sandbox"""
         self._validate_auth_headers()
         response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}", headers=self._auth_headers
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}",
+            headers=self._auth_headers,
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get sandbox component details.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
+                f"Failed to get sandbox component details.\n"
+                f"component id: '{component_id}'\n"
+                f"sandbox id: '{sandbox_id}'"
             )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
-    def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> list:
-        """ Get list of commands for a particular component in sandbox """
+    def get_sandbox_component_commands(
+        self, sandbox_id: str, component_id: str
+    ) -> list:
+        """Get list of commands for a particular component in sandbox"""
         self._validate_auth_headers()
         response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands", headers=self._auth_headers
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands",
+            headers=self._auth_headers,
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get component commands.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
+                f"Failed to get component commands.\n"
+                f"component id: '{component_id}'\n"
+                f"sandbox id: '{sandbox_id}'"
             )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
-    def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str) -> dict:
-        """ Get details of a command of sandbox component """
+    def get_sandbox_component_command_details(
+        self, sandbox_id: str, component_id: str, command: str
+    ) -> dict:
+        """Get details of a command of sandbox component"""
         self._validate_auth_headers()
         response = requests.get(
             f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}",
@@ -329,22 +430,33 @@ def get_sandbox_component_command_details(self, sandbox_id: str, component_id: s
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get command details.\n " f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
+                f"Failed to get command details.\n "
+                f"component id: '{component_id}'\n"
+                f"sandbox id: '{sandbox_id}'"
             )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
     def get_sandbox_instructions(self, sandbox_id: str) -> str:
-        """ pull the instruction text of sandbox """
+        """pull the instruction text of sandbox"""
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions", headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions",
+            headers=self._auth_headers,
+        )
         if not response.ok:
             err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'"
             raise SandboxRestException(err_msg, response)
         return response.json()
 
-    def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: int = None, since: str = None) -> dict:
-        """ Get list of sandbox output """
+    def get_sandbox_output(
+        self,
+        sandbox_id: str,
+        tail: int = None,
+        from_entry_id: int = None,
+        since: str = None,
+    ) -> dict:
+        """Get list of sandbox output"""
         self._validate_auth_headers()
         url = f"{self._versioned_url}/sandboxes/{sandbox_id}/output"
         params = {}
@@ -367,9 +479,11 @@ def get_sandbox_output(self, sandbox_id: str, tail: int = None, from_entry_id: i
 
     # BLUEPRINT GET REQUESTS
     def get_blueprints(self) -> list:
-        """ Get list of blueprints """
+        """Get list of blueprints"""
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/blueprints", headers=self._auth_headers
+        )
         if not response.ok:
             raise SandboxRestException("Failed to get blueprints", response)
         return response.json()
@@ -380,17 +494,27 @@ def get_blueprint_details(self, blueprint_id: str) -> dict:
         Can pass either blueprint name OR blueprint ID
         """
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/blueprints/{blueprint_id}", headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/blueprints/{blueprint_id}",
+            headers=self._auth_headers,
+        )
         if not response.ok:
-            raise SandboxRestException(f"Failed to get blueprint data for '{blueprint_id}'", response)
+            raise SandboxRestException(
+                f"Failed to get blueprint data for '{blueprint_id}'", response
+            )
         return response.json()
 
     # EXECUTIONS
     def get_execution_details(self, execution_id: str) -> dict:
         self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
+        response = requests.get(
+            f"{self._versioned_url}/executions/{execution_id}",
+            headers=self._auth_headers,
+        )
         if not response.ok:
-            raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response)
+            raise SandboxRestException(
+                f"Failed to get execution details for '{execution_id}'", response
+            )
         return response.json()
 
     def delete_execution(self, execution_id: str) -> None:
@@ -399,9 +523,17 @@ def delete_execution(self, execution_id: str) -> None:
         {"result": "success"}
         """
         self._validate_auth_headers()
-        response = requests.delete(f"{self._versioned_url}/executions/{execution_id}", headers=self._auth_headers)
+        response = requests.delete(
+            f"{self._versioned_url}/executions/{execution_id}",
+            headers=self._auth_headers,
+        )
         if not response.ok:
-            raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response)
+            raise SandboxRestException(
+                f"Failed to delete execution for '{execution_id}'", response
+            )
         response_dict = response.json()
         if not response_dict["result"] == "success":
-            raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}")
+            raise SandboxRestException(
+                f"Failed execution deletion of id {execution_id}\n"
+                f"Response: {response_dict}"
+            )
diff --git a/setup.py b/setup.py
index dfb0c59..84ba2e5 100644
--- a/setup.py
+++ b/setup.py
@@ -26,7 +26,7 @@ def lines_from_file(file_name: str) -> List[str]:
     packages=find_packages(),
     version=read_file("version.txt"),
     long_description=read_file("README.MD"),
-    long_description_content_type='text/markdown',
+    long_description_content_type="text/markdown",
     install_requires=lines_from_file("requirements.txt"),
     test_requires=lines_from_file("test-requirements.txt"),
     classifiers=[
diff --git a/tests/conftest.py b/tests/conftest.py
index dadf93a..99d0c61 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,7 +10,10 @@
 @pytest.fixture(scope="session")
 def admin_session() -> SandboxRestApiSession:
     with SandboxRestApiSession(
-        host=CLOUDSHELL_SERVER, username=CLOUDSHELL_ADMIN_USER, password=CLOUDSHELL_ADMIN_PASSWORD, domain=CLOUDSHELL_DOMAIN
+        host=CLOUDSHELL_SERVER,
+        username=CLOUDSHELL_ADMIN_USER,
+        password=CLOUDSHELL_ADMIN_PASSWORD,
+        domain=CLOUDSHELL_DOMAIN,
     ) as api:
         yield api
         time.sleep(2)
diff --git a/tests/test_api _no_sandbox.py b/tests/test_api _no_sandbox.py
index 4131083..048c9cf 100644
--- a/tests/test_api _no_sandbox.py	
+++ b/tests/test_api _no_sandbox.py	
@@ -27,7 +27,7 @@ def test_get_default_blueprint(admin_session: SandboxRestApiSession):
 
 
 def test_get_and_delete_token(admin_session: SandboxRestApiSession):
-    """ get token for admin user """
+    """get token for admin user"""
     token_res = admin_session.get_token_for_target_user("admin")
     assert type(token_res) is str
     print(f"Token response: '{token_res}'")
diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py
index b490bba..c367b75 100644
--- a/tests/test_api_empty_sandbox.py
+++ b/tests/test_api_empty_sandbox.py
@@ -20,7 +20,9 @@ def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint):
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     # start sandbox
-    start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test")
+    start_res = admin_session.start_sandbox(
+        blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test"
+    )
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -42,7 +44,9 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
         counter += 1
 
     print("Rerunning Setup...")
-    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND)
+    res = admin_session.run_sandbox_command(
+        sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND
+    )
     assert type(res) is dict
     print("Setup re-run execution response")
     pretty_print_response(res)
@@ -73,8 +77,13 @@ def test_get_sandbox_commands(admin_session, sandbox_id):
     commands_res = admin_session.get_sandbox_commands(sandbox_id)
     assert type(commands_res) is list
     print(f"Sandbox commands: {[x['name'] for x in commands_res]}")
-    first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, commands_res[0]["name"])
-    print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}")
+    first_sb_command = admin_session.get_sandbox_command_details(
+        sandbox_id, commands_res[0]["name"]
+    )
+    print(
+        f"SB command name: {first_sb_command['name']}\n"
+        f"description: {first_sb_command['description']}"
+    )
 
 
 def test_get_sandbox_events(admin_session, sandbox_id):
diff --git a/tests/test_api_heath_check_sandbox.py b/tests/test_api_heath_check_sandbox.py
index 43bded9..61882d7 100644
--- a/tests/test_api_heath_check_sandbox.py
+++ b/tests/test_api_heath_check_sandbox.py
@@ -23,7 +23,9 @@ def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint):
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     # start sandbox
-    start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test")
+    start_res = admin_session.start_sandbox(
+        blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test"
+    )
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -32,7 +34,9 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
 
 
 @pytest.fixture(scope="module")
-def component_id(admin_session: SandboxRestApiSession, sandbox_id: str, dut_blueprint: str):
+def component_id(
+    admin_session: SandboxRestApiSession, sandbox_id: str, dut_blueprint: str
+):
     components = admin_session.get_sandbox_components(sandbox_id)
     component_filter = [x for x in components if x["component_type"] == DUT_MODEL]
     assert component_filter
@@ -40,9 +44,13 @@ def component_id(admin_session: SandboxRestApiSession, sandbox_id: str, dut_blue
 
 
 @pytest.fixture(scope="module")
-def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str):
+def execution_id(
+    admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str
+):
     print("Starting DUT Command...")
-    res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND)
+    res = admin_session.run_component_command(
+        sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND
+    )
     assert type(res) is dict
     print("Started execution response")
     pretty_print_response(res)
diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py
index 12da642..c546269 100644
--- a/tests/test_rerun_setup.py
+++ b/tests/test_rerun_setup.py
@@ -13,7 +13,9 @@
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint):
     # start sandbox
-    start_res = admin_session.start_sandbox(blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test")
+    start_res = admin_session.start_sandbox(
+        blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test"
+    )
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -35,7 +37,9 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
         counter += 1
 
     print("Rerunning Setup...")
-    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND)
+    res = admin_session.run_sandbox_command(
+        sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND
+    )
     assert type(res) is dict
     print("Setup re-run execution response")
     pretty_print_response(res)

From 8baa18e477371107f6d7f5dd50215dfde2c2e45c Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 15:55:07 +0200
Subject: [PATCH 25/47] all linting passed!

---
 .pre-commit-config.yaml                       |  30 ++--
 cloudshell/sandbox_rest/exceptions.py         |  16 +-
 cloudshell/sandbox_rest/sandbox_api.py        | 140 +++++-------------
 ...eck_sandbox.py => test_api_dut_sandbox.py} |  30 ++--
 tests/test_api_empty_sandbox.py               |  39 ++---
 ... _no_sandbox.py => test_api_no_sandbox.py} |   9 +-
 tests/test_rerun_setup.py                     |  12 +-
 7 files changed, 92 insertions(+), 184 deletions(-)
 rename tests/{test_api_heath_check_sandbox.py => test_api_dut_sandbox.py} (75%)
 rename tests/{test_api _no_sandbox.py => test_api_no_sandbox.py} (85%)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d05e301..b8d53d5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,28 +4,28 @@ repos:
     hooks:
       - id: isort
         language_version: python3.7
-        args: [--line-length=127]
+        args: [ --line-length=127 ]
   - repo: https://github.com/python/black
     rev: 20.8b1
     hooks:
       - id: black
         language_version: python3.7
-        args: [--line-length=127]
+        args: [ --line-length=127 ]
   - repo: https://gitlab.com/pycqa/flake8
     rev: 3.8.3
     hooks:
       - id: flake8
         additional_dependencies: [
-          flake8-docstrings,
-          flake8-builtins,
-          flake8-comprehensions,
-          flake8-print,
-          flake8-eradicate,
+            flake8-docstrings,
+            flake8-builtins,
+            flake8-comprehensions,
+            flake8-print,
+            flake8-eradicate,
         ]
         language_version: python3.7
         args: [
-          --max-line-length=127,
-          '--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D200,D210,D401,W503,E203'
+            --max-line-length=127,
+            '--ignore=D100,D101,D102,D103,D104,D105,D106,D107,D200,D205,D210,D400,D401,W503,E203,F403,F405,T001'
         ]
   # See https://stackoverflow.com/questions/61238318/pylint-and-pre-commit-hook-unable-to-import/61238571#61238571
   - repo: local
@@ -34,11 +34,11 @@ repos:
         name: pylint
         entry: pylint
         language: system
-        types: [python]
+        types: [ python ]
         args: [
-#          --max-line-length=127,
-#          --max-public-methods=32,
-#          --max-args=8,
-          '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding',
-          '--good-names=ip,rc,eval'
+            --max-line-length=127,
+            --max-public-methods=32,
+            --max-args=9,
+            '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding, unused-wildcard-import,missing-function-docstring,missing-module-docstring,import-error,wildcard-import,invalid-name,redefined-outer-name,no-name-in-module',
+            '--good-names=ip,rc,eval'
         ]
\ No newline at end of file
diff --git a/cloudshell/sandbox_rest/exceptions.py b/cloudshell/sandbox_rest/exceptions.py
index c4154ff..d1f8359 100644
--- a/cloudshell/sandbox_rest/exceptions.py
+++ b/cloudshell/sandbox_rest/exceptions.py
@@ -2,22 +2,22 @@
 
 
 class SandboxRestException(Exception):
-    """Base Exception Class inside Rest client class"""
+    """ Base Exception Class inside Rest client class """
 
-    def __init__(self, msg: str, response: Response = None):
-        self.msg = msg
+    def __init__(self, message: str, response: Response = None):
+        super().__init__(message)
+        self.message = message
         self.response = response
 
     def __str__(self):
-        return self._format_err_msg(self.msg, self.response)
+        return self._format_err_msg(self.message, self.response)
 
-    def _format_err_msg(
-        self, custom_err_msg="Failed Api Call", response: Response = None
-    ) -> str:
+    def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = None) -> str:
         err_msg = f"Sandbox API Error: {custom_err_msg}"
 
         if response:
             err_msg += f"\n{self._format_response_msg(response)}"
+        return err_msg
 
     @staticmethod
     def _format_response_msg(response: Response):
@@ -30,5 +30,3 @@ def _format_response_msg(response: Response):
 
 class SandboxRestAuthException(SandboxRestException):
     """Failed auth action"""
-
-    pass
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index c673ad0..35e2588 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -4,10 +4,7 @@
 
 import requests
 
-from cloudshell.sandbox_rest.exceptions import (
-    SandboxRestAuthException,
-    SandboxRestException,
-)
+from cloudshell.sandbox_rest.exceptions import SandboxRestAuthException, SandboxRestException
 
 
 @dataclass
@@ -38,7 +35,7 @@ def __init__(
         is_https=False,
         api_version="v2",
     ):
-        """login to api and store headers for future requests"""
+        """ Login to api and store headers for future requests """
         _protocol = "https" if is_https else "http"
         self._base_url = f"{_protocol}://{host}:{port}/api"
         self._versioned_url = f"{self._base_url}/{api_version}"
@@ -47,9 +44,7 @@ def __init__(
         self._password = password
         self.domain = domain
         if self.username and self._password:
-            self.auth_token = self.get_token_with_credentials(
-                username, password, domain
-            )
+            self.auth_token = self.get_token_with_credentials(username, password, domain)
         elif token:
             self.auth_token = token
         self._auth_headers = self._build_auth_headers(self.auth_token)
@@ -65,7 +60,7 @@ def __exit__(self, exc_type, exc_value, traceback):
     @staticmethod
     def _build_auth_headers(login_token: str) -> dict:
         """
-        interpolate token into auth_headers dict
+        Interpolate token into auth_headers dict
         """
         auth_headers = {
             "Authorization": f"Basic {login_token}",
@@ -74,9 +69,7 @@ def _build_auth_headers(login_token: str) -> dict:
         return auth_headers
 
     def refresh_auth_from_stored_credentials(self) -> None:
-        self.auth_token = self.get_token_with_credentials(
-            self.username, self._password, self.domain
-        )
+        self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain)
         self._auth_headers = self._build_auth_headers(self.auth_token)
 
     def invalidate_auth(self) -> None:
@@ -85,22 +78,16 @@ def invalidate_auth(self) -> None:
 
     def _validate_auth_headers(self) -> None:
         if not self._auth_headers:
-            raise SandboxRestAuthException(
-                f"No auth headers currently set for '{self.username}' session"
-            )
+            raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session")
 
     # LOGIN METHODS
-    def get_token_with_credentials(
-        self, user_name: str, password: str, domain: str
-    ) -> str:
+    def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str:
         """
         Get token from credentials - extraneous quotes stripped off token string
         """
         response = requests.put(
             url=f"{self._base_url}/login",
-            data=json.dumps(
-                {"username": user_name, "password": password, "domain": domain}
-            ),
+            data=json.dumps({"username": user_name, "password": password, "domain": domain}),
             headers={"Content-Type": "application/json"},
         )
 
@@ -122,18 +109,14 @@ def get_token_for_target_user(self, user_name: str) -> str:
         )
 
         if not response.ok:
-            raise SandboxRestException(
-                f"Failed to get get token for user {user_name}", response
-            )
+            raise SandboxRestException(f"Failed to get get token for user {user_name}", response)
 
         login_token = response.text[1:-1]
         return login_token
 
     def delete_token(self, token_id: str) -> None:
         self._validate_auth_headers()
-        response = requests.delete(
-            url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers
-        )
+        response = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException("Failed to delete token", response)
 
@@ -152,11 +135,7 @@ def start_sandbox(
         """
         self._validate_auth_headers()
         url = f"{self._versioned_url}/blueprints/{blueprint_id}/start"
-        sandbox_name = (
-            sandbox_name
-            if sandbox_name
-            else self.get_blueprint_details(blueprint_id)["name"]
-        )
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
 
         data_dict = {
             "duration": duration,
@@ -165,9 +144,7 @@ def start_sandbox(
             "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
-        response = requests.post(
-            url, headers=self._auth_headers, data=json.dumps(data_dict)
-        )
+        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
         if not response.ok:
             custom_msg = f"Failed to start '{blueprint_id}'. "
             if 400 <= response.status_code < 500:
@@ -188,24 +165,16 @@ def start_persistent_sandbox(
         self._validate_auth_headers()
         url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent"
 
-        sandbox_name = (
-            sandbox_name
-            if sandbox_name
-            else self.get_blueprint_details(blueprint_id)["name"]
-        )
+        sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
         data_dict = {
             "name": sandbox_name,
             "permitted_users": permitted_users if permitted_users else [],
             "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
-        response = requests.post(
-            url, headers=self._auth_headers, data=json.dumps(data_dict)
-        )
+        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
         if not response.ok:
-            err_msg = (
-                f"Failed to start persistent sandbox from blueprint '{blueprint_id}'"
-            )
+            err_msg = f"Failed to start persistent sandbox from blueprint '{blueprint_id}'"
             raise SandboxRestException(err_msg, response)
         return response.json()
 
@@ -222,13 +191,9 @@ def run_sandbox_command(
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
-        response = requests.post(
-            url, data=json.dumps(data_dict), headers=self._auth_headers
-        )
+        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                f"failed to start sandbox command '{command_name}'", response
-            )
+            raise SandboxRestException(f"failed to start sandbox command '{command_name}'", response)
         return response.json()
 
     def run_component_command(
@@ -245,13 +210,9 @@ def run_component_command(
         data_dict = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
         data_dict["params"] = params
-        response = requests.post(
-            url, data=json.dumps(data_dict), headers=self._auth_headers
-        )
+        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
         if not response.ok:
-            raise SandboxRestException(
-                f"failed to start component command '{command_name}'", response
-            )
+            raise SandboxRestException(f"failed to start component command '{command_name}'", response)
         return response.json()
 
     def extend_sandbox(self, sandbox_id: str, duration: str) -> dict:
@@ -268,9 +229,7 @@ def extend_sandbox(self, sandbox_id: str, duration: str) -> dict:
             headers=self._auth_headers,
         )
         if not response.ok:
-            raise SandboxRestException(
-                f"failed to extend sandbox '{sandbox_id}'", response
-            )
+            raise SandboxRestException(f"failed to extend sandbox '{sandbox_id}'", response)
         return response.json()
 
     def stop_sandbox(self, sandbox_id: str) -> None:
@@ -281,9 +240,7 @@ def stop_sandbox(self, sandbox_id: str) -> None:
             headers=self._auth_headers,
         )
         if not response.ok:
-            raise SandboxRestException(
-                f"Failed to stop sandbox '{sandbox_id}'", response
-            )
+            raise SandboxRestException(f"Failed to stop sandbox '{sandbox_id}'", response)
 
     # SANDBOX GET REQUESTS
     def get_sandboxes(self, show_historic=False) -> list:
@@ -341,9 +298,7 @@ def get_sandbox_activity(
             response = requests.get(url, headers=self._auth_headers)
 
         if not response.ok:
-            raise SandboxRestException(
-                f"Failed to get sandbox activity for '{sandbox_id}'", response
-            )
+            raise SandboxRestException(f"Failed to get sandbox activity for '{sandbox_id}'", response)
         return response.json()
 
     def get_sandbox_commands(self, sandbox_id: str) -> list:
@@ -354,9 +309,7 @@ def get_sandbox_commands(self, sandbox_id: str) -> list:
             headers=self._auth_headers,
         )
         if not response.ok:
-            raise SandboxRestException(
-                f"Failed to get sandbox commands for '{sandbox_id}'", response
-            )
+            raise SandboxRestException(f"Failed to get sandbox commands for '{sandbox_id}'", response)
         return response.json()
 
     def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dict:
@@ -367,9 +320,7 @@ def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dic
             headers=self._auth_headers,
         )
         if not response.ok:
-            err_msg = (
-                f"Failed to get command details for '{command_name}' in '{sandbox_id}'"
-            )
+            err_msg = f"Failed to get command details for '{command_name}' in '{sandbox_id}'"
             raise SandboxRestException(err_msg, response)
         return response.json()
 
@@ -394,16 +345,12 @@ def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> d
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get sandbox component details.\n"
-                f"component id: '{component_id}'\n"
-                f"sandbox id: '{sandbox_id}'"
+                f"Failed to get sandbox component details.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
             )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
-    def get_sandbox_component_commands(
-        self, sandbox_id: str, component_id: str
-    ) -> list:
+    def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> list:
         """Get list of commands for a particular component in sandbox"""
         self._validate_auth_headers()
         response = requests.get(
@@ -412,16 +359,12 @@ def get_sandbox_component_commands(
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get component commands.\n"
-                f"component id: '{component_id}'\n"
-                f"sandbox id: '{sandbox_id}'"
+                f"Failed to get component commands.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
             )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
-    def get_sandbox_component_command_details(
-        self, sandbox_id: str, component_id: str, command: str
-    ) -> dict:
+    def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str) -> dict:
         """Get details of a command of sandbox component"""
         self._validate_auth_headers()
         response = requests.get(
@@ -430,15 +373,13 @@ def get_sandbox_component_command_details(
         )
         if not response.ok:
             custom_err_msg = (
-                f"Failed to get command details.\n "
-                f"component id: '{component_id}'\n"
-                f"sandbox id: '{sandbox_id}'"
+                f"Failed to get command details.\n " f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
             )
             raise SandboxRestException(custom_err_msg, response)
         return response.json()
 
     def get_sandbox_instructions(self, sandbox_id: str) -> str:
-        """pull the instruction text of sandbox"""
+        """ Pull the instructions text of sandbox """
         self._validate_auth_headers()
         response = requests.get(
             f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions",
@@ -481,9 +422,7 @@ def get_sandbox_output(
     def get_blueprints(self) -> list:
         """Get list of blueprints"""
         self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/blueprints", headers=self._auth_headers
-        )
+        response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers)
         if not response.ok:
             raise SandboxRestException("Failed to get blueprints", response)
         return response.json()
@@ -499,9 +438,7 @@ def get_blueprint_details(self, blueprint_id: str) -> dict:
             headers=self._auth_headers,
         )
         if not response.ok:
-            raise SandboxRestException(
-                f"Failed to get blueprint data for '{blueprint_id}'", response
-            )
+            raise SandboxRestException(f"Failed to get blueprint data for '{blueprint_id}'", response)
         return response.json()
 
     # EXECUTIONS
@@ -512,9 +449,7 @@ def get_execution_details(self, execution_id: str) -> dict:
             headers=self._auth_headers,
         )
         if not response.ok:
-            raise SandboxRestException(
-                f"Failed to get execution details for '{execution_id}'", response
-            )
+            raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response)
         return response.json()
 
     def delete_execution(self, execution_id: str) -> None:
@@ -528,12 +463,7 @@ def delete_execution(self, execution_id: str) -> None:
             headers=self._auth_headers,
         )
         if not response.ok:
-            raise SandboxRestException(
-                f"Failed to delete execution for '{execution_id}'", response
-            )
+            raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response)
         response_dict = response.json()
         if not response_dict["result"] == "success":
-            raise SandboxRestException(
-                f"Failed execution deletion of id {execution_id}\n"
-                f"Response: {response_dict}"
-            )
+            raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}")
diff --git a/tests/test_api_heath_check_sandbox.py b/tests/test_api_dut_sandbox.py
similarity index 75%
rename from tests/test_api_heath_check_sandbox.py
rename to tests/test_api_dut_sandbox.py
index 61882d7..7f9d61e 100644
--- a/tests/test_api_heath_check_sandbox.py
+++ b/tests/test_api_dut_sandbox.py
@@ -1,5 +1,6 @@
 """
-Test the api methods against blueprint with a resource containing a command
+Test the api methods against blueprint with a resource containing a command.
+
 - Putshell mock can be used - https://community.quali.com/repos/3318/put-shell-mock
 - DUT model / command can be referenced in constants.py (Putshell / health_check)
 - Assumed that only one DUT is in blueprint
@@ -16,16 +17,14 @@
 @pytest.fixture(scope="module")
 def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint):
     res_id = get_blueprint_id_from_name(admin_session, dut_blueprint)
-    assert type(res_id) is str
+    assert isinstance(res_id, str)
     return res_id
 
 
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     # start sandbox
-    start_res = admin_session.start_sandbox(
-        blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test"
-    )
+    start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test")
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -34,9 +33,7 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
 
 
 @pytest.fixture(scope="module")
-def component_id(
-    admin_session: SandboxRestApiSession, sandbox_id: str, dut_blueprint: str
-):
+def component_id(admin_session: SandboxRestApiSession, sandbox_id: str):
     components = admin_session.get_sandbox_components(sandbox_id)
     component_filter = [x for x in components if x["component_type"] == DUT_MODEL]
     assert component_filter
@@ -44,14 +41,10 @@ def component_id(
 
 
 @pytest.fixture(scope="module")
-def execution_id(
-    admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str
-):
+def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str):
     print("Starting DUT Command...")
-    res = admin_session.run_component_command(
-        sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND
-    )
-    assert type(res) is dict
+    res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND)
+    assert isinstance(res, dict)
     print("Started execution response")
     pretty_print_response(res)
     execution_id = res["executionId"]
@@ -61,12 +54,13 @@ def execution_id(
 @pytest.fixture(scope="module")
 def test_get_execution_details(admin_session, execution_id):
     res = admin_session.get_execution_details(execution_id)
-    assert type(res) is dict
-    print("Execution Details")
-    pretty_print_response(res)
+    assert isinstance(res, dict)
+    return res
 
 
 def test_delete_execution(admin_session, execution_id, test_get_execution_details):
+    print("Execution Details")
+    pretty_print_response(test_get_execution_details)
     print("Stopping execution...")
     time.sleep(1)
     admin_session.delete_execution(execution_id)
diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py
index c367b75..e326f0b 100644
--- a/tests/test_api_empty_sandbox.py
+++ b/tests/test_api_empty_sandbox.py
@@ -13,16 +13,14 @@
 @pytest.fixture(scope="module")
 def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint):
     res_id = get_blueprint_id_from_name(admin_session, empty_blueprint)
-    assert type(res_id) is str
+    assert isinstance(res_id, str)
     return res_id
 
 
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     # start sandbox
-    start_res = admin_session.start_sandbox(
-        blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test"
-    )
+    start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest empty blueprint test")
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -44,69 +42,62 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
         counter += 1
 
     print("Rerunning Setup...")
-    res = admin_session.run_sandbox_command(
-        sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND
-    )
-    assert type(res) is dict
+    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND)
+    assert isinstance(res, dict)
     print("Setup re-run execution response")
     pretty_print_response(res)
     execution_id = res["executionId"]
     return execution_id
 
 
-def test_start_stop(admin_session, sandbox_id):
-    assert type(sandbox_id) is str
+def test_start_stop(sandbox_id):
+    assert isinstance(sandbox_id, str)
     print(f"Sandbox ID: {sandbox_id}")
 
 
 def test_get_sandbox_details(admin_session, sandbox_id):
     details_res = admin_session.get_sandbox_details(sandbox_id)
-    assert type(details_res) is dict
+    assert isinstance(details_res, dict)
     sb_name = details_res["name"]
     print(f"Pulled details for sandbox '{sb_name}'")
 
 
 def test_get_components(admin_session, sandbox_id):
     components_res = admin_session.get_sandbox_components(sandbox_id)
-    assert type(components_res) is list
+    assert isinstance(components_res, list)
     component_count = len(components_res)
     print(f"component count found: {component_count}")
 
 
 def test_get_sandbox_commands(admin_session, sandbox_id):
     commands_res = admin_session.get_sandbox_commands(sandbox_id)
-    assert type(commands_res) is list
+    assert isinstance(commands_res, list)
     print(f"Sandbox commands: {[x['name'] for x in commands_res]}")
-    first_sb_command = admin_session.get_sandbox_command_details(
-        sandbox_id, commands_res[0]["name"]
-    )
-    print(
-        f"SB command name: {first_sb_command['name']}\n"
-        f"description: {first_sb_command['description']}"
-    )
+    first_sb_command = admin_session.get_sandbox_command_details(sandbox_id, commands_res[0]["name"])
+    print(f"SB command name: {first_sb_command['name']}\n" f"description: {first_sb_command['description']}")
 
 
 def test_get_sandbox_events(admin_session, sandbox_id):
     activity_res = admin_session.get_sandbox_activity(sandbox_id)
-    assert type(activity_res) is dict and "events" in activity_res
+    assert isinstance(activity_res, dict) and "events" in activity_res
     events = activity_res["events"]
     print(f"activity events count: {len(events)}")
 
 
 def test_get_console_output(admin_session, sandbox_id):
     output_res = admin_session.get_sandbox_output(sandbox_id)
-    assert type(output_res) is dict and "entries" in output_res
+    assert isinstance(output_res, dict) and "entries" in output_res
     entries = output_res["entries"]
     print(f"Sandbox output entries count: {len(entries)}")
 
 
 def test_get_instructions(admin_session, sandbox_id):
     instructions_res = admin_session.get_sandbox_instructions(sandbox_id)
-    assert type(instructions_res) is str
+    assert isinstance(instructions_res, str)
     print(f"Pulled sandbox instructions: '{instructions_res}'")
 
 
 def test_extend_sandbox(admin_session, sandbox_id):
     extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M")
-    assert type(extend_response) is dict and "remaining_time" in extend_response
+    assert isinstance(extend_response, dict) and "remaining_time" in extend_response
     print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}")
diff --git a/tests/test_api _no_sandbox.py b/tests/test_api_no_sandbox.py
similarity index 85%
rename from tests/test_api _no_sandbox.py
rename to tests/test_api_no_sandbox.py
index 048c9cf..f586407 100644
--- a/tests/test_api _no_sandbox.py	
+++ b/tests/test_api_no_sandbox.py
@@ -9,25 +9,24 @@
 
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
     res = admin_session.get_sandboxes()
-    assert type(res) is list
+    assert isinstance(res, list)
     print(f"Sandbox count found in system: {len(res)}")
 
 
 def test_get_blueprints(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprints()
-    assert type(bp_res) is list
+    assert isinstance(bp_res, list)
     print(f"Blueprint count found in system: '{len(bp_res)}'")
 
 
 def test_get_default_blueprint(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT)
-    assert type(bp_res) is dict
+    assert isinstance(bp_res, dict)
     bp_name = bp_res["name"]
     print(f"Pulled details for '{bp_name}'")
 
 
 def test_get_and_delete_token(admin_session: SandboxRestApiSession):
-    """get token for admin user"""
     token_res = admin_session.get_token_for_target_user("admin")
-    assert type(token_res) is str
+    assert isinstance(token_res, str)
     print(f"Token response: '{token_res}'")
diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py
index c546269..1ae31b1 100644
--- a/tests/test_rerun_setup.py
+++ b/tests/test_rerun_setup.py
@@ -1,5 +1,5 @@
 """
-Test re-running setup to validate blueprint level commands, getting details, and then ending setup execution, finally end sandbox
+Test re-running setup to validate blueprint level commands, getting details, and then ending setup execution
 """
 import time
 
@@ -13,9 +13,7 @@
 @pytest.fixture(scope="module")
 def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint):
     # start sandbox
-    start_res = admin_session.start_sandbox(
-        blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test"
-    )
+    start_res = admin_session.start_sandbox(blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test")
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
     yield sandbox_id
@@ -37,10 +35,8 @@ def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
         counter += 1
 
     print("Rerunning Setup...")
-    res = admin_session.run_sandbox_command(
-        sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND
-    )
-    assert type(res) is dict
+    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND)
+    assert isinstance(res, dict)
     print("Setup re-run execution response")
     pretty_print_response(res)
     execution_id = res["executionId"]

From 5755e95aa9de97f62219f82d2425fc4dfa71fcd8 Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 16:04:27 +0200
Subject: [PATCH 26/47] Delete cloudshell/__pycache__ directory

---
 cloudshell/__pycache__/__init__.cpython-37.pyc | Bin 270 -> 0 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 cloudshell/__pycache__/__init__.cpython-37.pyc

diff --git a/cloudshell/__pycache__/__init__.cpython-37.pyc b/cloudshell/__pycache__/__init__.cpython-37.pyc
deleted file mode 100644
index 1638dc996911c33a01b65615684ced948aabc95d..0000000000000000000000000000000000000000
GIT binary patch

From 7ec339860b494141151860d15fdf38ef837bfbca Mon Sep 17 00:00:00 2001
From: Natti Katz <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 16:05:02 +0200
Subject: [PATCH 27/47] Delete cloudshell/sandbox_rest/__pycache__ directory

---
 .../__pycache__/__init__.cpython-37.pyc         | Bin 283 -> 0 bytes
 .../__pycache__/__init__.cpython-39.pyc         | Bin 287 -> 0 bytes
 .../__pycache__/sandbox_api.cpython-37.pyc      | Bin 17475 -> 0 bytes
 .../__pycache__/sandbox_api.cpython-39.pyc      | Bin 13066 -> 0 bytes
 4 files changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc
 delete mode 100644 cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc
 delete mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc
 delete mode 100644 cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc

diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-37.pyc
deleted file mode 100644
index eee6736ad4ff2f82925fd57e10453058b115b48f..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc b/cloudshell/sandbox_rest/__pycache__/__init__.cpython-39.pyc
deleted file mode 100644
index 25178204b2e5b5ce4876c516480741b828ffc7da..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-37.pyc
deleted file mode 100644
index 0c82e772dd292885b28a57a3cf3496288ebef2a9..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc b/cloudshell/sandbox_rest/__pycache__/sandbox_api.cpython-39.pyc
deleted file mode 100644
index 2eef5552e21c5b945ca34ec9e8843b988fe1aa3b..0000000000000000000000000000000000000000
GIT binary patch

From b096ac959a32fc81a46c9845e2a54fecee083b86 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 16:05:33 +0200
Subject: [PATCH 28/47] modify git ignore

---
 .gitignore | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 9ae8ac3..6e01c40 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@
 *.egg-info
 tests/.env
 *.zip
-dist/
\ No newline at end of file
+dist/
+*.pyc
\ No newline at end of file

From 51a8512077076bc853123e230f07dcc5538ba5ab Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 16:11:59 +0200
Subject: [PATCH 29/47] black formatted init

---
 cloudshell/sandbox_rest/sandbox_api.py | 10 +---------
 1 file changed, 1 insertion(+), 9 deletions(-)

diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index 35e2588..eb671a7 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -25,15 +25,7 @@ class SandboxRestApiSession:
     """
 
     def __init__(
-        self,
-        host: str,
-        username="",
-        password="",
-        token="",
-        domain="Global",
-        port=82,
-        is_https=False,
-        api_version="v2",
+        self, host: str, username="", password="", token="", domain="Global", port=82, is_https=False, api_version="v2"
     ):
         """ Login to api and store headers for future requests """
         _protocol = "https" if is_https else "http"

From b028c410b5881a62ee07d234ff276c037e19a460 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 16:21:57 +0200
Subject: [PATCH 30/47] added utility token refresh method

---
 cloudshell/sandbox_rest/sandbox_api.py | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/cloudshell/sandbox_rest/sandbox_api.py
index eb671a7..f87d619 100644
--- a/cloudshell/sandbox_rest/sandbox_api.py
+++ b/cloudshell/sandbox_rest/sandbox_api.py
@@ -64,6 +64,10 @@ def refresh_auth_from_stored_credentials(self) -> None:
         self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain)
         self._auth_headers = self._build_auth_headers(self.auth_token)
 
+    def refresh_auth_from_token(self, token: str) -> None:
+        self.auth_token = token
+        self._auth_headers = self._build_auth_headers(token)
+
     def invalidate_auth(self) -> None:
         self.delete_token(self.auth_token)
         self._auth_headers = None

From 675b9b78815d22435257a473b57d4a9c79f3a4d9 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sat, 27 Nov 2021 17:59:28 +0200
Subject: [PATCH 31/47] added new test

---
 .gitignore                   |  4 +++-
 LICENSE                      | 22 ++++++++++++++++++++++
 test-requirements.txt        |  3 ++-
 tests/test_api_no_sandbox.py | 30 ++++++++++++++++++++++++------
 4 files changed, 51 insertions(+), 8 deletions(-)
 create mode 100644 LICENSE

diff --git a/.gitignore b/.gitignore
index 6e01c40..96702e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,6 @@
 tests/.env
 *.zip
 dist/
-*.pyc
\ No newline at end of file
+build/
+*.pyc
+venv
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..93ee38a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+
+MIT License
+
+Copyright (c) 2021
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/test-requirements.txt b/test-requirements.txt
index 2ed62ba..ff6f3e3 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,4 +1,5 @@
 pytest
 python-dotenv
 flake8
-pylint
\ No newline at end of file
+pylint
+pre-commit
\ No newline at end of file
diff --git a/tests/test_api_no_sandbox.py b/tests/test_api_no_sandbox.py
index f586407..f12c19b 100644
--- a/tests/test_api_no_sandbox.py
+++ b/tests/test_api_no_sandbox.py
@@ -2,11 +2,35 @@
 Test the api methods that do NOT require a blueprint
 Live Cloudshell server is still a dependency
 """
+import pytest
 from constants import DEFAULT_EMPTY_BLUEPRINT
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
+@pytest.fixture(scope="module")
+def api_token(admin_session: SandboxRestApiSession):
+    token = admin_session.get_token_for_target_user("admin")
+    return token
+
+
+def test_delete_token(admin_session: SandboxRestApiSession, api_token: str):
+    assert isinstance(api_token, str)
+    print(f"Token response: '{api_token}'")
+    admin_session.delete_token(api_token)
+
+
+def test_reset_session(admin_session: SandboxRestApiSession):
+    print("\ndeleting admin token...")
+    admin_session.invalidate_auth()
+    print("logging back in..")
+    admin_session.refresh_auth_from_stored_credentials()
+    print("Calling get blueprints...")
+    res = admin_session.get_blueprints()
+    assert isinstance(res, list)
+    print(f"blueprint count {len(res)}")
+
+
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
     res = admin_session.get_sandboxes()
     assert isinstance(res, list)
@@ -24,9 +48,3 @@ def test_get_default_blueprint(admin_session: SandboxRestApiSession):
     assert isinstance(bp_res, dict)
     bp_name = bp_res["name"]
     print(f"Pulled details for '{bp_name}'")
-
-
-def test_get_and_delete_token(admin_session: SandboxRestApiSession):
-    token_res = admin_session.get_token_for_target_user("admin")
-    assert isinstance(token_res, str)
-    print(f"Token response: '{token_res}'")

From c79e90d1103bfd15da7590d8b8523ac5a40bc0dd Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 28 Nov 2021 04:53:43 +0200
Subject: [PATCH 32/47] refactored project files

---
 pyproject.toml                                |  3 ++
 test-requirements.txt => requirements-dev.txt |  0
 requirements.txt                              |  3 +-
 setup.cfg                                     | 27 ++++++++++
 setup.py                                      | 40 ++------------
 {cloudshell => src/cloudshell}/__init__.py    |  0
 .../cloudshell}/sandbox_rest/__init__.py      |  0
 .../cloudshell}/sandbox_rest/exceptions.py    |  0
 .../cloudshell}/sandbox_rest/sandbox_api.py   |  0
 tests/common.py                               |  2 +-
 tests/test_rerun_setup.py                     | 52 -------------------
 11 files changed, 35 insertions(+), 92 deletions(-)
 create mode 100644 pyproject.toml
 rename test-requirements.txt => requirements-dev.txt (100%)
 create mode 100644 setup.cfg
 rename {cloudshell => src/cloudshell}/__init__.py (100%)
 rename {cloudshell => src/cloudshell}/sandbox_rest/__init__.py (100%)
 rename {cloudshell => src/cloudshell}/sandbox_rest/exceptions.py (100%)
 rename {cloudshell => src/cloudshell}/sandbox_rest/sandbox_api.py (100%)
 delete mode 100644 tests/test_rerun_setup.py

diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..1b68d94
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
\ No newline at end of file
diff --git a/test-requirements.txt b/requirements-dev.txt
similarity index 100%
rename from test-requirements.txt
rename to requirements-dev.txt
diff --git a/requirements.txt b/requirements.txt
index e886ed4..37f34de 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1 @@
-requests
-retrying
\ No newline at end of file
+requests>=2,<3
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..06172dc
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,27 @@
+[metadata]
+name = cloudshell_sandboxapi_wrapper
+version = file: version.txt
+author = QualiLab
+author_email = support@qualisystems.com
+description = Python client for CloudShell Sandbox REST api - consume sandboxes via CI
+long_description = file: README.MD
+long_description_content_type = text/markdown
+url = https://github.com/QualiSystemsLab/Sandbox-API-Python
+classifiers =
+    Programming Language :: Python :: 3.7
+    License :: OSI Approved :: MIT License
+    Operating System :: OS Independent
+license = MIT
+license_file = LICENSE
+
+
+[options]
+package_dir =
+    = src
+packages = find:
+python_requires = >=3.7
+install_requires =
+    requests>=2
+
+[options.packages.find]
+where = src
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 84ba2e5..da0fa5e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,38 +1,4 @@
-from typing import List
+from setuptools import setup
 
-from setuptools import find_packages, setup
-
-
-def read_file(file_name: str) -> str:
-    with open(file_name) as f:
-        content = f.read().strip()
-    return content
-
-
-def lines_from_file(file_name: str) -> List[str]:
-    with open(file_name) as f:
-        lines = f.read().splitlines()
-    return lines
-
-
-setup(
-    name="cloudshell_sandboxapi_wrapper",
-    description="Python client for CloudShell Sandbox REST api - consume sandboxes via CI",
-    keywords=["cloudshell", "sandbox", "api", "rest", "CI"],
-    url="https://github.com/QualiSystemsLab/Sandbox-API-Python",
-    author="sadanand.s",
-    author_email="sadanand.s@quali.com",
-    license="Apache 2.0",
-    packages=find_packages(),
-    version=read_file("version.txt"),
-    long_description=read_file("README.MD"),
-    long_description_content_type="text/markdown",
-    install_requires=lines_from_file("requirements.txt"),
-    test_requires=lines_from_file("test-requirements.txt"),
-    classifiers=[
-        "License :: OSI Approved :: Apache Software License",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-    ],
-)
+if __name__ == "__main__":
+    setup()
diff --git a/cloudshell/__init__.py b/src/cloudshell/__init__.py
similarity index 100%
rename from cloudshell/__init__.py
rename to src/cloudshell/__init__.py
diff --git a/cloudshell/sandbox_rest/__init__.py b/src/cloudshell/sandbox_rest/__init__.py
similarity index 100%
rename from cloudshell/sandbox_rest/__init__.py
rename to src/cloudshell/sandbox_rest/__init__.py
diff --git a/cloudshell/sandbox_rest/exceptions.py b/src/cloudshell/sandbox_rest/exceptions.py
similarity index 100%
rename from cloudshell/sandbox_rest/exceptions.py
rename to src/cloudshell/sandbox_rest/exceptions.py
diff --git a/cloudshell/sandbox_rest/sandbox_api.py b/src/cloudshell/sandbox_rest/sandbox_api.py
similarity index 100%
rename from cloudshell/sandbox_rest/sandbox_api.py
rename to src/cloudshell/sandbox_rest/sandbox_api.py
diff --git a/tests/common.py b/tests/common.py
index 962b5ce..6b97c3c 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1,6 +1,6 @@
 import json
 
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+from src.cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 def pretty_print_response(dict_response):
diff --git a/tests/test_rerun_setup.py b/tests/test_rerun_setup.py
deleted file mode 100644
index 1ae31b1..0000000
--- a/tests/test_rerun_setup.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""
-Test re-running setup to validate blueprint level commands, getting details, and then ending setup execution
-"""
-import time
-
-import pytest
-from common import *
-from constants import *
-
-from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
-
-
-@pytest.fixture(scope="module")
-def sandbox_id(admin_session: SandboxRestApiSession, empty_blueprint):
-    # start sandbox
-    start_res = admin_session.start_sandbox(blueprint_id=empty_blueprint, sandbox_name="Pytest empty blueprint test")
-    sandbox_id = start_res["id"]
-    print(f"Sandbox started: {sandbox_id}")
-    yield sandbox_id
-    admin_session.stop_sandbox(sandbox_id)
-    print(f"\nSandbox ended: {sandbox_id}")
-
-
-@pytest.fixture(scope="module")
-def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
-    polling_minutes = 2
-    counter = 0
-    while True:
-        if counter > polling_minutes:
-            raise Exception("Timeout waiting for setup to end")
-        state = admin_session.get_sandbox_details(sandbox_id)["state"]
-        if state == "Ready":
-            break
-        time.sleep(60)
-        counter += 1
-
-    print("Rerunning Setup...")
-    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND)
-    assert isinstance(res, dict)
-    print("Setup re-run execution response")
-    pretty_print_response(res)
-    execution_id = res["executionId"]
-    return execution_id
-
-
-def test_cancel_setup(admin_session, setup_execution_id):
-    print("Setup Execution Details")
-    res = admin_session.get_execution_details(setup_execution_id)
-    pretty_print_response(res)
-    print("Ending setup execution")
-    admin_session.delete_execution(setup_execution_id)
-    print("Setup execution cancelled")

From 91736928e55c4f2910a8f19be0b6545f438d96fa Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Mon, 6 Dec 2021 21:30:30 +0200
Subject: [PATCH 33/47] tests passing

---
 .pre-commit-config.yaml                    |   4 +-
 requirements.txt                           |   3 +-
 src/cloudshell/sandbox_rest/exceptions.py  |  28 +-
 src/cloudshell/sandbox_rest/sandbox_api.py | 392 +++++++--------------
 tests/common.py                            |  13 +
 tests/conftest.py                          |  15 +-
 tests/test_api_dut_sandbox.py              |  12 +-
 tests/test_api_empty_sandbox.py            |  31 +-
 tests/test_api_no_sandbox.py               |  15 +-
 9 files changed, 181 insertions(+), 332 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b8d53d5..f6badf9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -38,7 +38,7 @@ repos:
         args: [
             --max-line-length=127,
             --max-public-methods=32,
-            --max-args=9,
-            '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding, unused-wildcard-import,missing-function-docstring,missing-module-docstring,import-error,wildcard-import,invalid-name,redefined-outer-name,no-name-in-module',
+            --max-args=13,
+            '--disable=too-few-public-methods,logging-fstring-interpolation,too-many-instance-attributes,no-else-return,too-many-locals,no-self-use,duplicate-code,broad-except,logging-not-lazy,unspecified-encoding, unused-wildcard-import,missing-function-docstring,missing-module-docstring,import-error,wildcard-import,invalid-name,redefined-outer-name,no-name-in-module, arguments-differ',
             '--good-names=ip,rc,eval'
         ]
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 37f34de..9bcc92b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
-requests>=2,<3
\ No newline at end of file
+requests>=2,<3
+abstract-http-client
\ No newline at end of file
diff --git a/src/cloudshell/sandbox_rest/exceptions.py b/src/cloudshell/sandbox_rest/exceptions.py
index d1f8359..befbf51 100644
--- a/src/cloudshell/sandbox_rest/exceptions.py
+++ b/src/cloudshell/sandbox_rest/exceptions.py
@@ -1,32 +1,6 @@
-from requests import Response
-
-
 class SandboxRestException(Exception):
     """ Base Exception Class inside Rest client class """
 
-    def __init__(self, message: str, response: Response = None):
-        super().__init__(message)
-        self.message = message
-        self.response = response
-
-    def __str__(self):
-        return self._format_err_msg(self.message, self.response)
-
-    def _format_err_msg(self, custom_err_msg="Failed Api Call", response: Response = None) -> str:
-        err_msg = f"Sandbox API Error: {custom_err_msg}"
-
-        if response:
-            err_msg += f"\n{self._format_response_msg(response)}"
-        return err_msg
-
-    @staticmethod
-    def _format_response_msg(response: Response):
-        return (
-            f"Response: {response.status_code}, Reason: {response.reason}\n"
-            f"Request URL: {response.request.url}\n"
-            f"Request Headers: {response.request.headers}"
-        )
-
 
 class SandboxRestAuthException(SandboxRestException):
-    """Failed auth action"""
+    """ Failed login action """
diff --git a/src/cloudshell/sandbox_rest/sandbox_api.py b/src/cloudshell/sandbox_rest/sandbox_api.py
index f87d619..ff732e6 100644
--- a/src/cloudshell/sandbox_rest/sandbox_api.py
+++ b/src/cloudshell/sandbox_rest/sandbox_api.py
@@ -1,8 +1,9 @@
 import json
+import logging
 from dataclasses import asdict, dataclass
 from typing import List
 
-import requests
+from abstract_http_client.http_clients.requests_client import RequestsClient
 
 from cloudshell.sandbox_rest.exceptions import SandboxRestAuthException, SandboxRestException
 
@@ -18,103 +19,95 @@ class InputParam:
     value: str
 
 
-class SandboxRestApiSession:
+class SandboxRestApiSession(RequestsClient):
     """
     Python wrapper for CloudShell Sandbox API
     View http://<API_SERVER>/api/v2/explore to see schemas of return json values
     """
 
     def __init__(
-        self, host: str, username="", password="", token="", domain="Global", port=82, is_https=False, api_version="v2"
+        self,
+        host: str,
+        username="",
+        password="",
+        domain="Global",
+        token="",
+        port=82,
+        logger: logging.Logger = None,
+        use_https=False,
+        ssl_verify=False,
+        proxies: dict = None,
+        insecure_warning=False,
     ):
         """ Login to api and store headers for future requests """
-        _protocol = "https" if is_https else "http"
-        self._base_url = f"{_protocol}://{host}:{port}/api"
-        self._versioned_url = f"{self._base_url}/{api_version}"
-        self.host = host
-        self.username = username
-        self._password = password
+        super().__init__(host, username, password, token, logger, port, use_https, ssl_verify, proxies, insecure_warning)
+        self._base_uri = "/api"
+        self._v2_base_uri = f"{self._base_uri}/v2"
         self.domain = domain
-        if self.username and self._password:
-            self.auth_token = self.get_token_with_credentials(username, password, domain)
-        elif token:
-            self.auth_token = token
-        self._auth_headers = self._build_auth_headers(self.auth_token)
-
-    # CONTEXT MANAGER METHODS
-    def __enter__(self):
-        return self
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        # invalidate stored api token on exit
-        self.invalidate_auth()
-
-    @staticmethod
-    def _build_auth_headers(login_token: str) -> dict:
-        """
-        Interpolate token into auth_headers dict
-        """
-        auth_headers = {
-            "Authorization": f"Basic {login_token}",
-            "Content-Type": "application/json",
-        }
-        return auth_headers
+        self.login()
+
+    def login(self, user="", password="", token="", domain="") -> None:
+        self.user = user or self.user
+        self.password = password or self.password
+        self.token = token or self.token
+        self.domain = domain or self.domain
 
-    def refresh_auth_from_stored_credentials(self) -> None:
-        self.auth_token = self.get_token_with_credentials(self.username, self._password, self.domain)
-        self._auth_headers = self._build_auth_headers(self.auth_token)
+        if not self.domain:
+            raise ValueError("Domain must be passed to login")
 
-    def refresh_auth_from_token(self, token: str) -> None:
-        self.auth_token = token
-        self._auth_headers = self._build_auth_headers(token)
+        if not self.token and self.user and self.password:
+            self.token = self._get_token_with_credentials(self.user, self.password, self.domain)
+        else:
+            raise ValueError("Login requires Token or Username / Password")
 
-    def invalidate_auth(self) -> None:
-        self.delete_token(self.auth_token)
-        self._auth_headers = None
+        self._set_auth_header_on_session()
 
-    def _validate_auth_headers(self) -> None:
-        if not self._auth_headers:
-            raise SandboxRestAuthException(f"No auth headers currently set for '{self.username}' session")
+    def logout(self) -> None:
+        if not self.token:
+            return
+        self.delete_token(self.token)
+        self.token = None
+        self._remove_auth_header_from_session()
 
-    # LOGIN METHODS
-    def get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str:
+    def _get_token_with_credentials(self, user_name: str, password: str, domain: str) -> str:
         """
         Get token from credentials - extraneous quotes stripped off token string
         """
-        response = requests.put(
-            url=f"{self._base_url}/login",
-            data=json.dumps({"username": user_name, "password": password, "domain": domain}),
-            headers={"Content-Type": "application/json"},
-        )
-
-        if not response.ok:
-            raise SandboxRestAuthException("Failed Login", response)
+        uri = f"{self._base_uri}/login"
+        data = {"username": user_name, "password": password, "domain": domain}
+        response = self.rest_service.request_put(uri, data)
 
         login_token = response.text[1:-1]
+        if not login_token:
+            raise SandboxRestAuthException(f"Invalid token. Token response {response.text}")
+
         return login_token
 
+    def _set_auth_header_on_session(self):
+        self.rest_service.session.headers.update({"Authorization": f"Basic {self.token}"})
+
+    def _remove_auth_header_from_session(self):
+        self.rest_service.session.headers.pop("Authorization")
+
+    def _validate_auth_header(self) -> None:
+        if not self.rest_service.session.headers.get("Authorization"):
+            raise SandboxRestAuthException("No Authorization header currently set for session")
+
     def get_token_for_target_user(self, user_name: str) -> str:
         """
         Get token for target user - remove extraneous quotes
         """
-        self._validate_auth_headers()
-        response = requests.post(
-            url=f"{self._base_url}/token",
-            data=json.dumps({"username": user_name}),
-            headers=self._auth_headers,
-        )
-
-        if not response.ok:
-            raise SandboxRestException(f"Failed to get get token for user {user_name}", response)
-
+        self._validate_auth_header()
+        uri = f"{self._base_uri}/token"
+        data = {"username": user_name}
+        response = self.rest_service.request_post(uri, data)
         login_token = response.text[1:-1]
         return login_token
 
     def delete_token(self, token_id: str) -> None:
-        self._validate_auth_headers()
-        response = requests.delete(url=f"{self._base_url}/token/{token_id}", headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException("Failed to delete token", response)
+        self._validate_auth_header()
+        uri = f"{self._base_uri}/token/{token_id}"
+        return self.rest_service.request_delete(uri).text
 
     # SANDBOX POST REQUESTS
     def start_sandbox(
@@ -129,26 +122,18 @@ def start_sandbox(
         Create a sandbox from the provided blueprint id
         Duration format must be a valid 'ISO 8601'. (e.g 'PT23H' or 'PT4H2M')
         """
-        self._validate_auth_headers()
-        url = f"{self._versioned_url}/blueprints/{blueprint_id}/start"
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/blueprints/{blueprint_id}/start"
         sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
 
-        data_dict = {
+        data = {
             "duration": duration,
             "name": sandbox_name,
             "permitted_users": permitted_users if permitted_users else [],
             "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
-        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
-        if not response.ok:
-            custom_msg = f"Failed to start '{blueprint_id}'. "
-            if 400 <= response.status_code < 500:
-                custom_msg += "Ensure blueprint is 'Public' and in correct domain."
-            if response.status_code >= 500:
-                custom_msg += "Internal Server 500 Error During Request."
-            raise SandboxRestException(custom_msg, response)
-        return response.json()
+        return self.rest_service.request_post(uri, data).json()
 
     def start_persistent_sandbox(
         self,
@@ -157,22 +142,18 @@ def start_persistent_sandbox(
         bp_params: List[InputParam] = None,
         permitted_users: List[str] = None,
     ) -> dict:
-        """Create a persistent sandbox from the provided blueprint id"""
-        self._validate_auth_headers()
-        url = f"{self._versioned_url}/blueprints/{blueprint_id}/start-persistent"
+        """ Create a persistent sandbox from the provided blueprint id """
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/blueprints/{blueprint_id}/start-persistent"
 
         sandbox_name = sandbox_name if sandbox_name else self.get_blueprint_details(blueprint_id)["name"]
-        data_dict = {
+        data = {
             "name": sandbox_name,
             "permitted_users": permitted_users if permitted_users else [],
             "params": [asdict(x) for x in bp_params] if bp_params else [],
         }
 
-        response = requests.post(url, headers=self._auth_headers, data=json.dumps(data_dict))
-        if not response.ok:
-            err_msg = f"Failed to start persistent sandbox from blueprint '{blueprint_id}'"
-            raise SandboxRestException(err_msg, response)
-        return response.json()
+        return self.rest_service.request_post(uri, data).json()
 
     def run_sandbox_command(
         self,
@@ -182,15 +163,12 @@ def run_sandbox_command(
         print_output=True,
     ) -> dict:
         """Run a sandbox level command"""
-        self._validate_auth_headers()
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}/start"
-        data_dict = {"printOutput": print_output}
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/commands/{command_name}/start"
+        data = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
-        data_dict["params"] = params
-        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(f"failed to start sandbox command '{command_name}'", response)
-        return response.json()
+        data["params"] = params
+        return self.rest_service.request_post(uri, data).json()
 
     def run_component_command(
         self,
@@ -201,15 +179,12 @@ def run_component_command(
         print_output: bool = True,
     ) -> dict:
         """Start a command on sandbox component"""
-        self._validate_auth_headers()
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start"
-        data_dict = {"printOutput": print_output}
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command_name}/start"
+        data = {"printOutput": print_output}
         params = [asdict(x) for x in params] if params else []
-        data_dict["params"] = params
-        response = requests.post(url, data=json.dumps(data_dict), headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException(f"failed to start component command '{command_name}'", response)
-        return response.json()
+        data["params"] = params
+        return self.rest_service.request_post(uri, data).json()
 
     def extend_sandbox(self, sandbox_id: str, duration: str) -> dict:
         """Extend the sandbox
@@ -217,48 +192,30 @@ def extend_sandbox(self, sandbox_id: str, duration: str) -> dict:
         :param str duration: duration in ISO 8601 format (P1Y1M1DT1H1M1S = 1year, 1month, 1day, 1hour, 1min, 1sec)
         :return:
         """
-        self._validate_auth_headers()
-        data_dict = {"extended_time": duration}
-        response = requests.post(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/extend",
-            data=json.dumps(data_dict),
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            raise SandboxRestException(f"failed to extend sandbox '{sandbox_id}'", response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/extend"
+        data = {"extended_time": duration}
+        return self.rest_service.request_post(uri, data).json()
 
     def stop_sandbox(self, sandbox_id: str) -> None:
         """Stop the sandbox given sandbox id"""
-        self._validate_auth_headers()
-        response = requests.post(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/stop",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            raise SandboxRestException(f"Failed to stop sandbox '{sandbox_id}'", response)
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/stop"
+        return self.rest_service.request_post(uri).json()
 
     # SANDBOX GET REQUESTS
     def get_sandboxes(self, show_historic=False) -> list:
         """Get list of sandboxes"""
-        self._validate_auth_headers()
-        url = f"{self._versioned_url}/sandboxes"
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes"
         params = {"show_historic": "true" if show_historic else "false"}
-        response = requests.get(url, headers=self._auth_headers, params=params)
-        if not response.ok:
-            err_msg = "Failed to get sandboxes"
-            raise SandboxRestException(err_msg, response)
-        return response.json()
+        return self.rest_service.request_get(uri, params=params).json()
 
     def get_sandbox_details(self, sandbox_id: str) -> dict:
         """Get details of the given sandbox id"""
-        self._validate_auth_headers()
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}"
-        response = requests.get(url, headers=self._auth_headers)
-        if not response.ok:
-            exc_msg = f"Failed to get sandbox details for '{sandbox_id}'"
-            raise SandboxRestException(exc_msg, response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}"
+        return self.rest_service.request_get(uri).json()
 
     def get_sandbox_activity(
         self,
@@ -275,8 +232,8 @@ def get_sandbox_activity(
         'tail' - how many of the last entries you want to pull
         'error_only' - to filter for error events only
         """
-        self._validate_auth_headers()
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/activity"
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/activity"
         params = {}
 
         if error_only:
@@ -288,103 +245,49 @@ def get_sandbox_activity(
         if tail:
             params["tail"] = tail
 
-        if params:
-            response = requests.get(url, headers=self._auth_headers, params=params)
-        else:
-            response = requests.get(url, headers=self._auth_headers)
-
-        if not response.ok:
-            raise SandboxRestException(f"Failed to get sandbox activity for '{sandbox_id}'", response)
-        return response.json()
+        return self.rest_service.request_get(uri, params=params).json()
 
     def get_sandbox_commands(self, sandbox_id: str) -> list:
         """Get list of sandbox commands"""
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/commands",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            raise SandboxRestException(f"Failed to get sandbox commands for '{sandbox_id}'", response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/commands"
+        return self.rest_service.request_get(uri).json()
 
     def get_sandbox_command_details(self, sandbox_id: str, command_name: str) -> dict:
         """Get details of specific sandbox command"""
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/commands/{command_name}",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            err_msg = f"Failed to get command details for '{command_name}' in '{sandbox_id}'"
-            raise SandboxRestException(err_msg, response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/commands/{command_name}"
+        return self.rest_service.request_get(uri).json()
 
     def get_sandbox_components(self, sandbox_id: str) -> list:
         """Get list of sandbox components"""
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            err_msg = (response, f"Failed to get components for '{sandbox_id}'")
-            raise SandboxRestException(err_msg, response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components"
+        return self.rest_service.request_get(uri).json()
 
     def get_sandbox_component_details(self, sandbox_id: str, component_id: str) -> dict:
         """Get details of components in sandbox"""
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            custom_err_msg = (
-                f"Failed to get sandbox component details.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
-            )
-            raise SandboxRestException(custom_err_msg, response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components/{component_id}"
+        return self.rest_service.request_get(uri).json()
 
     def get_sandbox_component_commands(self, sandbox_id: str, component_id: str) -> list:
         """Get list of commands for a particular component in sandbox"""
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            custom_err_msg = (
-                f"Failed to get component commands.\n" f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
-            )
-            raise SandboxRestException(custom_err_msg, response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components/{component_id}/commands"
+        return self.rest_service.request_get(uri).json()
 
     def get_sandbox_component_command_details(self, sandbox_id: str, component_id: str, command: str) -> dict:
         """Get details of a command of sandbox component"""
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            custom_err_msg = (
-                f"Failed to get command details.\n " f"component id: '{component_id}'\n" f"sandbox id: '{sandbox_id}'"
-            )
-            raise SandboxRestException(custom_err_msg, response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/components/{component_id}/commands/{command}"
+        return self.rest_service.request_get(uri).json()
 
     def get_sandbox_instructions(self, sandbox_id: str) -> str:
         """ Pull the instructions text of sandbox """
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/sandboxes/{sandbox_id}/instructions",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'"
-            raise SandboxRestException(err_msg, response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/instructions"
+        return self.rest_service.request_get(uri).json()
 
     def get_sandbox_output(
         self,
@@ -394,8 +297,8 @@ def get_sandbox_output(
         since: str = None,
     ) -> dict:
         """Get list of sandbox output"""
-        self._validate_auth_headers()
-        url = f"{self._versioned_url}/sandboxes/{sandbox_id}/output"
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/sandboxes/{sandbox_id}/output"
         params = {}
         if tail:
             params["tail"] = tail
@@ -404,62 +307,39 @@ def get_sandbox_output(
         if since:
             params["since"] = since
 
-        if params:
-            response = requests.get(url, headers=self._auth_headers, params=params)
-        else:
-            response = requests.get(url, headers=self._auth_headers)
-
-        if not response.ok:
-            err_msg = f"Failed to get sandbox instructions for '{sandbox_id}'"
-            raise SandboxRestException(err_msg, response)
-        return response.json()
+        return self.rest_service.request_get(uri, params=params).json()
 
     # BLUEPRINT GET REQUESTS
     def get_blueprints(self) -> list:
         """Get list of blueprints"""
-        self._validate_auth_headers()
-        response = requests.get(f"{self._versioned_url}/blueprints", headers=self._auth_headers)
-        if not response.ok:
-            raise SandboxRestException("Failed to get blueprints", response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/blueprints"
+        return self.rest_service.request_get(uri).json()
 
     def get_blueprint_details(self, blueprint_id: str) -> dict:
         """
         Get details of a specific blueprint
         Can pass either blueprint name OR blueprint ID
         """
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/blueprints/{blueprint_id}",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            raise SandboxRestException(f"Failed to get blueprint data for '{blueprint_id}'", response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/blueprints/{blueprint_id}"
+        return self.rest_service.request_get(uri).json()
 
     # EXECUTIONS
     def get_execution_details(self, execution_id: str) -> dict:
-        self._validate_auth_headers()
-        response = requests.get(
-            f"{self._versioned_url}/executions/{execution_id}",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            raise SandboxRestException(f"Failed to get execution details for '{execution_id}'", response)
-        return response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/executions/{execution_id}"
+        return self.rest_service.request_get(uri).json()
 
     def delete_execution(self, execution_id: str) -> None:
         """
         API returns dict with single key on successful deletion of execution
         {"result": "success"}
         """
-        self._validate_auth_headers()
-        response = requests.delete(
-            f"{self._versioned_url}/executions/{execution_id}",
-            headers=self._auth_headers,
-        )
-        if not response.ok:
-            raise SandboxRestException(f"Failed to delete execution for '{execution_id}'", response)
-        response_dict = response.json()
+        self._validate_auth_header()
+        uri = f"{self._v2_base_uri}/executions/{execution_id}"
+        response_dict = self.rest_service.request_delete(uri).json()
         if not response_dict["result"] == "success":
-            raise SandboxRestException(f"Failed execution deletion of id {execution_id}\n" f"Response: {response_dict}")
+            raise SandboxRestException(
+                f"Failed execution deletion of id {execution_id}\n" f"{json.dumps(response_dict, indent=4)}"
+            )
diff --git a/tests/common.py b/tests/common.py
index 6b97c3c..e6d6678 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1,4 +1,6 @@
 import json
+from random import randint
+from time import sleep
 
 from src.cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
@@ -8,6 +10,17 @@ def pretty_print_response(dict_response):
     print(f"\n{json_str}")
 
 
+def random_sleep():
+    """ To offset the api calls and avoid rate limit quota """
+    random = randint(1, 3)
+    print(f"sleeping {random} seconds")
+    sleep(random)
+
+
+def fixed_sleep():
+    sleep(3)
+
+
 def get_blueprint_id_from_name(api: SandboxRestApiSession, bp_name: str):
     res = api.get_blueprint_details(bp_name)
     return res["id"]
diff --git a/tests/conftest.py b/tests/conftest.py
index 99d0c61..0bbf68f 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,15 +9,14 @@
 
 @pytest.fixture(scope="session")
 def admin_session() -> SandboxRestApiSession:
-    with SandboxRestApiSession(
-        host=CLOUDSHELL_SERVER,
-        username=CLOUDSHELL_ADMIN_USER,
-        password=CLOUDSHELL_ADMIN_PASSWORD,
-        domain=CLOUDSHELL_DOMAIN,
-    ) as api:
-        yield api
-        time.sleep(2)
+    admin_api = SandboxRestApiSession(
+        host=CLOUDSHELL_SERVER, username=CLOUDSHELL_ADMIN_USER, password=CLOUDSHELL_ADMIN_PASSWORD, domain=CLOUDSHELL_DOMAIN
+    )
+    with admin_api:
+        yield admin_api
+        time.sleep(3)
         print("admin session token revoked")
+        print(f"total requests: {admin_api.rest_service.request_counter}")
 
 
 @pytest.fixture(scope="session")
diff --git a/tests/test_api_dut_sandbox.py b/tests/test_api_dut_sandbox.py
index 7f9d61e..35bf6a9 100644
--- a/tests/test_api_dut_sandbox.py
+++ b/tests/test_api_dut_sandbox.py
@@ -5,8 +5,6 @@
 - DUT model / command can be referenced in constants.py (Putshell / health_check)
 - Assumed that only one DUT is in blueprint
 """
-import time
-
 import pytest
 from common import *
 from constants import *
@@ -27,6 +25,7 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test")
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
+    fixed_sleep()
     yield sandbox_id
     admin_session.stop_sandbox(sandbox_id)
     print(f"\nSandbox ended: {sandbox_id}")
@@ -35,6 +34,7 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
 @pytest.fixture(scope="module")
 def component_id(admin_session: SandboxRestApiSession, sandbox_id: str):
     components = admin_session.get_sandbox_components(sandbox_id)
+    fixed_sleep()
     component_filter = [x for x in components if x["component_type"] == DUT_MODEL]
     assert component_filter
     return component_filter[0]["id"]
@@ -44,6 +44,7 @@ def component_id(admin_session: SandboxRestApiSession, sandbox_id: str):
 def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str):
     print("Starting DUT Command...")
     res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND)
+    fixed_sleep()
     assert isinstance(res, dict)
     print("Started execution response")
     pretty_print_response(res)
@@ -54,6 +55,7 @@ def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, componen
 @pytest.fixture(scope="module")
 def test_get_execution_details(admin_session, execution_id):
     res = admin_session.get_execution_details(execution_id)
+    fixed_sleep()
     assert isinstance(res, dict)
     return res
 
@@ -61,7 +63,11 @@ def test_get_execution_details(admin_session, execution_id):
 def test_delete_execution(admin_session, execution_id, test_get_execution_details):
     print("Execution Details")
     pretty_print_response(test_get_execution_details)
+    is_supports_cancellation = test_get_execution_details["supports_cancellation"]
+    if not is_supports_cancellation:
+        print("Can't cancel this command. Returning")
+        return
     print("Stopping execution...")
-    time.sleep(1)
     admin_session.delete_execution(execution_id)
+    fixed_sleep()
     print("Execution deleted")
diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py
index e326f0b..2681ad5 100644
--- a/tests/test_api_empty_sandbox.py
+++ b/tests/test_api_empty_sandbox.py
@@ -1,11 +1,9 @@
 """
 Test the api methods that require an empty, PUBLIC blueprint
 """
-import time
 
 import pytest
 from common import *
-from constants import *
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
@@ -28,34 +26,13 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     print(f"\nSandbox ended: {sandbox_id}")
 
 
-@pytest.fixture(scope="module")
-def setup_execution_id(admin_session: SandboxRestApiSession, sandbox_id: str):
-    polling_minutes = 2
-    counter = 0
-    while True:
-        if counter > polling_minutes:
-            raise Exception("Timeout waiting for setup to end")
-        state = admin_session.get_sandbox_details(sandbox_id)["state"]
-        if state == "Ready":
-            break
-        time.sleep(60)
-        counter += 1
-
-    print("Rerunning Setup...")
-    res = admin_session.run_sandbox_command(sandbox_id=sandbox_id, command_name=BLUEPRINT_SETUP_COMMAND)
-    assert isinstance(res, dict)
-    print("Setup re-run execution response")
-    pretty_print_response(res)
-    execution_id = res["executionId"]
-    return execution_id
-
-
 def test_start_stop(sandbox_id):
     assert isinstance(sandbox_id, str)
     print(f"Sandbox ID: {sandbox_id}")
 
 
 def test_get_sandbox_details(admin_session, sandbox_id):
+    random_sleep()
     details_res = admin_session.get_sandbox_details(sandbox_id)
     assert isinstance(details_res, dict)
     sb_name = details_res["name"]
@@ -63,6 +40,7 @@ def test_get_sandbox_details(admin_session, sandbox_id):
 
 
 def test_get_components(admin_session, sandbox_id):
+    random_sleep()
     components_res = admin_session.get_sandbox_components(sandbox_id)
     assert isinstance(components_res, list)
     component_count = len(components_res)
@@ -70,6 +48,7 @@ def test_get_components(admin_session, sandbox_id):
 
 
 def test_get_sandbox_commands(admin_session, sandbox_id):
+    random_sleep()
     commands_res = admin_session.get_sandbox_commands(sandbox_id)
     assert isinstance(commands_res, list)
     print(f"Sandbox commands: {[x['name'] for x in commands_res]}")
@@ -78,6 +57,7 @@ def test_get_sandbox_commands(admin_session, sandbox_id):
 
 
 def test_get_sandbox_events(admin_session, sandbox_id):
+    random_sleep()
     activity_res = admin_session.get_sandbox_activity(sandbox_id)
     assert isinstance(activity_res, dict) and "events" in activity_res
     events = activity_res["events"]
@@ -85,6 +65,7 @@ def test_get_sandbox_events(admin_session, sandbox_id):
 
 
 def test_get_console_output(admin_session, sandbox_id):
+    random_sleep()
     output_res = admin_session.get_sandbox_output(sandbox_id)
     assert isinstance(output_res, dict) and "entries" in output_res
     entries = output_res["entries"]
@@ -92,12 +73,14 @@ def test_get_console_output(admin_session, sandbox_id):
 
 
 def test_get_instructions(admin_session, sandbox_id):
+    random_sleep()
     instructions_res = admin_session.get_sandbox_instructions(sandbox_id)
     assert isinstance(instructions_res, str)
     print(f"Pulled sandbox instructions: '{instructions_res}'")
 
 
 def test_extend_sandbox(admin_session, sandbox_id):
+    random_sleep()
     extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M")
     assert isinstance(extend_response, dict) and "remaining_time" in extend_response
     print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}")
diff --git a/tests/test_api_no_sandbox.py b/tests/test_api_no_sandbox.py
index f12c19b..93f1f3e 100644
--- a/tests/test_api_no_sandbox.py
+++ b/tests/test_api_no_sandbox.py
@@ -3,6 +3,7 @@
 Live Cloudshell server is still a dependency
 """
 import pytest
+from common import *
 from constants import DEFAULT_EMPTY_BLUEPRINT
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
@@ -20,31 +21,23 @@ def test_delete_token(admin_session: SandboxRestApiSession, api_token: str):
     admin_session.delete_token(api_token)
 
 
-def test_reset_session(admin_session: SandboxRestApiSession):
-    print("\ndeleting admin token...")
-    admin_session.invalidate_auth()
-    print("logging back in..")
-    admin_session.refresh_auth_from_stored_credentials()
-    print("Calling get blueprints...")
-    res = admin_session.get_blueprints()
-    assert isinstance(res, list)
-    print(f"blueprint count {len(res)}")
-
-
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
     res = admin_session.get_sandboxes()
+    random_sleep()
     assert isinstance(res, list)
     print(f"Sandbox count found in system: {len(res)}")
 
 
 def test_get_blueprints(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprints()
+    random_sleep()
     assert isinstance(bp_res, list)
     print(f"Blueprint count found in system: '{len(bp_res)}'")
 
 
 def test_get_default_blueprint(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT)
+    random_sleep()
     assert isinstance(bp_res, dict)
     bp_name = bp_res["name"]
     print(f"Pulled details for '{bp_name}'")

From 81a2895a3ab02d00e22206397a22ae410aaa046f Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Tue, 21 Dec 2021 02:12:42 +0200
Subject: [PATCH 34/47] edit signature

---
 src/cloudshell/sandbox_rest/sandbox_api.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/cloudshell/sandbox_rest/sandbox_api.py b/src/cloudshell/sandbox_rest/sandbox_api.py
index ff732e6..203b9fa 100644
--- a/src/cloudshell/sandbox_rest/sandbox_api.py
+++ b/src/cloudshell/sandbox_rest/sandbox_api.py
@@ -37,10 +37,10 @@ def __init__(
         use_https=False,
         ssl_verify=False,
         proxies: dict = None,
-        insecure_warning=False,
+        show_insecure_warning=False,
     ):
         """ Login to api and store headers for future requests """
-        super().__init__(host, username, password, token, logger, port, use_https, ssl_verify, proxies, insecure_warning)
+        super().__init__(host, username, password, token, logger, port, use_https, ssl_verify, proxies, show_insecure_warning)
         self._base_uri = "/api"
         self._v2_base_uri = f"{self._base_uri}/v2"
         self.domain = domain

From 3499a4b3e4ea61c8e4524f2839721989b3b76d2e Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 19:38:51 +0200
Subject: [PATCH 35/47] reset version and change package name

---
 .github/workflows/lint-test.yml | 27 +++++++++++
 .github/workflows/release.yml   | 83 +++++++++++++++++++++++++++++++++
 requirements.txt                |  2 +-
 setup.cfg                       |  8 ++--
 version.txt                     |  2 +-
 5 files changed, 117 insertions(+), 5 deletions(-)
 create mode 100644 .github/workflows/lint-test.yml
 create mode 100644 .github/workflows/release.yml

diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml
new file mode 100644
index 0000000..70e84c9
--- /dev/null
+++ b/.github/workflows/lint-test.yml
@@ -0,0 +1,27 @@
+name: Lint and Test
+
+on:
+  pull_request:
+      branches: [master]
+  push:
+    branches: [master]
+
+jobs:
+  lint-test:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    - name: install ALL dependencies
+      run: |
+        pip install -U -r requirements-dev.txt
+        pip install -U -r requirements.txt
+        pip install .
+    - name: run pre-commit linters and formatters
+      uses: pre-commit/action@v2.0.3
+    - name: run pytest tests
+      run: python -m pytest --import-mode=append tests/ --cov
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..baca2f4
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,83 @@
+# setup tools guide used as base template - https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
+# wait on check action used to wait for tests to finish - https://github.com/marketplace/actions/wait-on-check
+name: Release
+
+on:
+  push:
+    tags:
+      - 'v*'
+
+jobs:
+  build-n-publish:
+    name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Wait for lint and tests to succeed
+      uses: lewagon/wait-on-check-action@v0.2
+      with:
+        ref: ${{ github.ref }}
+        check-name: 'lint-test'
+        repo-token: ${{ secrets.GITHUB_TOKEN }}
+        wait-interval: 10
+    
+    - name: Checkout Code
+      uses: actions/checkout@master
+    
+    - name: Validate tag version - compare version.txt to tag
+      run: |
+        version=$(cat ./version.txt)              
+        tag=${GITHUB_REF/refs\/tags\//}           
+        tag="${tag:1}"                            # remove the 'v' prefix from the tag that triggered this action
+        echo $version
+        echo $tag
+        if [ "$tag" == "$version" ]
+        then
+          echo "Tag and version are equal"
+        else
+          echo "Error: Tag and version are not equal, cannot create a release"
+          exit 1
+        fi
+
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+    
+    - name: Install pypa/build
+      run: >-
+        python -m
+        pip install
+        build
+        --user
+    
+    - name: Build a binary wheel and a source tarball
+      run: >-
+        python -m
+        build
+        --sdist
+        --wheel
+        --outdir dist/
+        .
+
+    - name: Publish distribution 📦 to Test PyPI
+      if: startsWith(github.ref, 'refs/tags')
+      uses: pypa/gh-action-pypi-publish@master
+      with:
+        password: ${{ secrets.TEST_PYPI_TOKEN }}
+        repository_url: https://test.pypi.org/legacy/
+        skip_existing: true  # skip duplicate uploads to test pypi
+
+    - name: Publish distribution 📦 to Real PyPI
+      if: startsWith(github.ref, 'refs/tags')
+      uses: pypa/gh-action-pypi-publish@master
+      with:
+        password: ${{ secrets.PYPI_API_TOKEN }}
+        
+      
+    - name: Create Github Release and Upload Artifacts
+      uses: softprops/action-gh-release@v1
+      with:
+        files: dist/*
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 9bcc92b..471039c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
 requests>=2,<3
-abstract-http-client
\ No newline at end of file
+abstract-http-client>=1,<2
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 06172dc..5c6ed9f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [metadata]
-name = cloudshell_sandboxapi_wrapper
+name = cloudshell-sandbox-rest
 version = file: version.txt
 author = QualiLab
 author_email = support@qualisystems.com
@@ -20,8 +20,10 @@ package_dir =
     = src
 packages = find:
 python_requires = >=3.7
-install_requires =
-    requests>=2
+install_requires =[
+    requests>=2, <3
+    abstract-http-client>=1,<2
+]
 
 [options.packages.find]
 where = src
\ No newline at end of file
diff --git a/version.txt b/version.txt
index 359a5b9..6c6aa7c 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-2.0.0
\ No newline at end of file
+0.1.0
\ No newline at end of file

From 9b072912974baf47955a1d69a3fadbd2017e7ec1 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 19:40:37 +0200
Subject: [PATCH 36/47] typo in setup.cfg

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index 5c6ed9f..f0dcf9b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -21,7 +21,7 @@ package_dir =
 packages = find:
 python_requires = >=3.7
 install_requires =[
-    requests>=2, <3
+    requests>=2, <3,
     abstract-http-client>=1,<2
 ]
 

From 8efd8c0132f2fed78624ea1b2773b473d1e70ac0 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 19:46:42 +0200
Subject: [PATCH 37/47] setup.cfg fix again

---
 setup.cfg | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index f0dcf9b..c261d10 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,10 +20,9 @@ package_dir =
     = src
 packages = find:
 python_requires = >=3.7
-install_requires =[
-    requests>=2, <3,
+install_requires =
+    requests>=2, <3
     abstract-http-client>=1,<2
-]
 
 [options.packages.find]
 where = src
\ No newline at end of file

From e4042f6d23ce9ba1e64b1a5e913e09aa0bddbf10 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 19:59:09 +0200
Subject: [PATCH 38/47] remove pytest from workflow

---
 .github/workflows/lint-test.yml | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml
index 70e84c9..a784600 100644
--- a/.github/workflows/lint-test.yml
+++ b/.github/workflows/lint-test.yml
@@ -1,4 +1,4 @@
-name: Lint and Test
+name: Pre-commit Lint
 
 on:
   pull_request:
@@ -22,6 +22,7 @@ jobs:
         pip install .
     - name: run pre-commit linters and formatters
       uses: pre-commit/action@v2.0.3
-    - name: run pytest tests
-      run: python -m pytest --import-mode=append tests/ --cov
+      # pytest runs integration tests with dependency on Quali Server Instance - run them locally
+#    - name: run pytest tests
+#      run: python -m pytest --import-mode=append tests/ --cov
 

From 199c183614c3b4d9ae6dc73350430d648205d6a8 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 21:41:59 +0200
Subject: [PATCH 39/47] usage examples added

---
 README.MD | 62 ++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 54 insertions(+), 8 deletions(-)

diff --git a/README.MD b/README.MD
index e64d887..a5fcc8e 100644
--- a/README.MD
+++ b/README.MD
@@ -1,20 +1,66 @@
 [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python/)
 [![PyPI version](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper.svg)](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper)
 
-# Cloudshell Sandbox API Wrapper
-A python client implementation of the [cloudshell sandbox api](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/CS-Snbx-API-Topic.htm?Highlight=sandbox%20api).
-
+# Cloudshell Sandbox Rest Api Client
+A python client wrapper around the [cloudshell sandbox api](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/CS-Snbx-API-Topic.htm?Highlight=sandbox%20api).
 This client provides an object-oriented interface to instantiating an api session and interacting with the endpoints.
-All methods return a loaded python dictionary/list of the [documented](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/RefGuides/Sndbx-REST-API/REST-API-V2-Ref-Guide.htm?tocpath=CloudShell%20API%20Guide%7CCloudShell%20Sandbox%20API%7C_____3) json responses. 
-No additional library object wrapping implemented. 
 
+No additional library object wrapping implemented. All methods return the json.loads python object in the shape of the documented json response. See the [documentation](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/RefGuides/Sndbx-REST-API/REST-API-V2-Ref-Guide.htm?tocpath=CloudShell%20API%20Guide%7CCloudShell%20Sandbox%20API%7C_____3) 
+for details. 
 
 ### Installation
 
 ```
-pip install cloudshell_sandboxapi_wrapper
+pip install cloudshell-sandbox-rest
 ```
-## Usage
+
+### Basic Usage
 ```python
-pass
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="Global")
+start_response = api.start_sandbox(blueprint_id="<MY_BLUEPRINT_NAME>", sandbox_name="My Rest Api Blueprint")
+sandbox_id = start_response["id"]
+components_response = api.get_sandbox_components(sandbox_id)
+print(f"total components in sandbox: {len(components_response)}")
 ```
+
+### Context Manager Usage
+Using the api session with a context manager "with" statement will log out and invalidate the token for you.
+
+```python
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+
+api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="Global")
+
+with api:
+    start_response = api.start_sandbox(blueprint_id="<MY_BLUEPRINT_NAME>", sandbox_name="My Rest Api Blueprint")
+    sandbox_id = start_response["id"]
+    components_response = api.get_sandbox_components(sandbox_id)
+    print(f"total components in sandbox: {len(components_response)}")
+    print(f"session token is: {api.token}")
+
+print(f"session token outside context manager: {api.token}")
+```
+- NOTE: api login happens during init, not on entering context
+- context exit invalidates token
+
+### Instantiate Session with Token
+Common use case is for admin to pull user token and start a session on their behalf. This can be done as seen in example below.
+```python
+from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+
+admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="Global")
+
+with admin_api:
+    user_token = admin_api.get_token_for_target_user("end_user")
+    user_api = SandboxRestApiSession(host="localhost", token=user_token, domain="<END_USERS_DOMAIN>")
+    with user_api:
+        start_response = user_api.start_sandbox(blueprint_id="<MY_BLUEPRINT_NAME>", sandbox_name="My Rest Api Blueprint")
+        sandbox_id = start_response["id"]
+        components_response = user_api.get_sandbox_components(sandbox_id)
+        print(f"total components in sandbox: {len(components_response)}")
+```
+- Note the use of nested context managers to manage the different session tokens
+
+### License 
+Free Software: MIT License
\ No newline at end of file

From e0b294ac454fbd20161c7c7d8a9a38ee9867a128 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 22:11:24 +0200
Subject: [PATCH 40/47] pre-commit reformat

---
 tests/constants.py              | 11 ++++++++---
 tests/test_api_dut_sandbox.py   | 26 ++++++++++++++------------
 tests/test_api_empty_sandbox.py | 18 +++++++++---------
 tests/test_api_no_sandbox.py    |  8 ++++----
 4 files changed, 35 insertions(+), 28 deletions(-)

diff --git a/tests/constants.py b/tests/constants.py
index 77becad..3d37424 100644
--- a/tests/constants.py
+++ b/tests/constants.py
@@ -1,7 +1,12 @@
-DEFAULT_EMPTY_BLUEPRINT = "CloudShell Sandbox Template"
-BLUEPRINT_SETUP_COMMAND = "Setup"
+# create these two blueprints and make them 'public' so they can be reserved
+DEFAULT_EMPTY_BLUEPRINT = "Empty Blueprint"
+
+# this blueprint has a Putshell Resource in it
+# https://community.quali.com/repos/3318/put-shell-mock
+DUT_BLUEPRINT = "DUT Blueprint Test"
 
 # DUT blueprint constants
 DUT_MODEL = "Putshell"
 DUT_COMMAND = "health_check"
-DUT_BLUEPRINT = "DUT Blueprint Test"
+
+BLUEPRINT_SETUP_COMMAND = "Setup"
diff --git a/tests/test_api_dut_sandbox.py b/tests/test_api_dut_sandbox.py
index 35bf6a9..9b6bc85 100644
--- a/tests/test_api_dut_sandbox.py
+++ b/tests/test_api_dut_sandbox.py
@@ -5,16 +5,16 @@
 - DUT model / command can be referenced in constants.py (Putshell / health_check)
 - Assumed that only one DUT is in blueprint
 """
+import common
+import constants
 import pytest
-from common import *
-from constants import *
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 @pytest.fixture(scope="module")
 def blueprint_id(admin_session: SandboxRestApiSession, dut_blueprint):
-    res_id = get_blueprint_id_from_name(admin_session, dut_blueprint)
+    res_id = common.get_blueprint_id_from_name(admin_session, dut_blueprint)
     assert isinstance(res_id, str)
     return res_id
 
@@ -25,7 +25,7 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
     start_res = admin_session.start_sandbox(blueprint_id=blueprint_id, sandbox_name="Pytest DUT blueprint test")
     sandbox_id = start_res["id"]
     print(f"Sandbox started: {sandbox_id}")
-    fixed_sleep()
+    common.fixed_sleep()
     yield sandbox_id
     admin_session.stop_sandbox(sandbox_id)
     print(f"\nSandbox ended: {sandbox_id}")
@@ -34,8 +34,8 @@ def sandbox_id(admin_session: SandboxRestApiSession, blueprint_id):
 @pytest.fixture(scope="module")
 def component_id(admin_session: SandboxRestApiSession, sandbox_id: str):
     components = admin_session.get_sandbox_components(sandbox_id)
-    fixed_sleep()
-    component_filter = [x for x in components if x["component_type"] == DUT_MODEL]
+    common.fixed_sleep()
+    component_filter = [x for x in components if x["component_type"] == constants.DUT_MODEL]
     assert component_filter
     return component_filter[0]["id"]
 
@@ -43,11 +43,13 @@ def component_id(admin_session: SandboxRestApiSession, sandbox_id: str):
 @pytest.fixture(scope="module")
 def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, component_id: str):
     print("Starting DUT Command...")
-    res = admin_session.run_component_command(sandbox_id=sandbox_id, component_id=component_id, command_name=DUT_COMMAND)
-    fixed_sleep()
+    res = admin_session.run_component_command(
+        sandbox_id=sandbox_id, component_id=component_id, command_name=constants.DUT_COMMAND
+    )
+    common.fixed_sleep()
     assert isinstance(res, dict)
     print("Started execution response")
-    pretty_print_response(res)
+    common.pretty_print_response(res)
     execution_id = res["executionId"]
     return execution_id
 
@@ -55,19 +57,19 @@ def execution_id(admin_session: SandboxRestApiSession, sandbox_id: str, componen
 @pytest.fixture(scope="module")
 def test_get_execution_details(admin_session, execution_id):
     res = admin_session.get_execution_details(execution_id)
-    fixed_sleep()
+    common.fixed_sleep()
     assert isinstance(res, dict)
     return res
 
 
 def test_delete_execution(admin_session, execution_id, test_get_execution_details):
     print("Execution Details")
-    pretty_print_response(test_get_execution_details)
+    common.pretty_print_response(test_get_execution_details)
     is_supports_cancellation = test_get_execution_details["supports_cancellation"]
     if not is_supports_cancellation:
         print("Can't cancel this command. Returning")
         return
     print("Stopping execution...")
     admin_session.delete_execution(execution_id)
-    fixed_sleep()
+    common.fixed_sleep()
     print("Execution deleted")
diff --git a/tests/test_api_empty_sandbox.py b/tests/test_api_empty_sandbox.py
index 2681ad5..e224e2b 100644
--- a/tests/test_api_empty_sandbox.py
+++ b/tests/test_api_empty_sandbox.py
@@ -2,15 +2,15 @@
 Test the api methods that require an empty, PUBLIC blueprint
 """
 
+import common
 import pytest
-from common import *
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
 
 @pytest.fixture(scope="module")
 def blueprint_id(admin_session: SandboxRestApiSession, empty_blueprint):
-    res_id = get_blueprint_id_from_name(admin_session, empty_blueprint)
+    res_id = common.get_blueprint_id_from_name(admin_session, empty_blueprint)
     assert isinstance(res_id, str)
     return res_id
 
@@ -32,7 +32,7 @@ def test_start_stop(sandbox_id):
 
 
 def test_get_sandbox_details(admin_session, sandbox_id):
-    random_sleep()
+    common.random_sleep()
     details_res = admin_session.get_sandbox_details(sandbox_id)
     assert isinstance(details_res, dict)
     sb_name = details_res["name"]
@@ -40,7 +40,7 @@ def test_get_sandbox_details(admin_session, sandbox_id):
 
 
 def test_get_components(admin_session, sandbox_id):
-    random_sleep()
+    common.random_sleep()
     components_res = admin_session.get_sandbox_components(sandbox_id)
     assert isinstance(components_res, list)
     component_count = len(components_res)
@@ -48,7 +48,7 @@ def test_get_components(admin_session, sandbox_id):
 
 
 def test_get_sandbox_commands(admin_session, sandbox_id):
-    random_sleep()
+    common.random_sleep()
     commands_res = admin_session.get_sandbox_commands(sandbox_id)
     assert isinstance(commands_res, list)
     print(f"Sandbox commands: {[x['name'] for x in commands_res]}")
@@ -57,7 +57,7 @@ def test_get_sandbox_commands(admin_session, sandbox_id):
 
 
 def test_get_sandbox_events(admin_session, sandbox_id):
-    random_sleep()
+    common.random_sleep()
     activity_res = admin_session.get_sandbox_activity(sandbox_id)
     assert isinstance(activity_res, dict) and "events" in activity_res
     events = activity_res["events"]
@@ -65,7 +65,7 @@ def test_get_sandbox_events(admin_session, sandbox_id):
 
 
 def test_get_console_output(admin_session, sandbox_id):
-    random_sleep()
+    common.random_sleep()
     output_res = admin_session.get_sandbox_output(sandbox_id)
     assert isinstance(output_res, dict) and "entries" in output_res
     entries = output_res["entries"]
@@ -73,14 +73,14 @@ def test_get_console_output(admin_session, sandbox_id):
 
 
 def test_get_instructions(admin_session, sandbox_id):
-    random_sleep()
+    common.random_sleep()
     instructions_res = admin_session.get_sandbox_instructions(sandbox_id)
     assert isinstance(instructions_res, str)
     print(f"Pulled sandbox instructions: '{instructions_res}'")
 
 
 def test_extend_sandbox(admin_session, sandbox_id):
-    random_sleep()
+    common.random_sleep()
     extend_response = admin_session.extend_sandbox(sandbox_id, "PT0H10M")
     assert isinstance(extend_response, dict) and "remaining_time" in extend_response
     print(f"extended sandbox. Remaining time: {extend_response['remaining_time']}")
diff --git a/tests/test_api_no_sandbox.py b/tests/test_api_no_sandbox.py
index 93f1f3e..2a80487 100644
--- a/tests/test_api_no_sandbox.py
+++ b/tests/test_api_no_sandbox.py
@@ -2,8 +2,8 @@
 Test the api methods that do NOT require a blueprint
 Live Cloudshell server is still a dependency
 """
+import common
 import pytest
-from common import *
 from constants import DEFAULT_EMPTY_BLUEPRINT
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
@@ -23,21 +23,21 @@ def test_delete_token(admin_session: SandboxRestApiSession, api_token: str):
 
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
     res = admin_session.get_sandboxes()
-    random_sleep()
+    common.random_sleep()
     assert isinstance(res, list)
     print(f"Sandbox count found in system: {len(res)}")
 
 
 def test_get_blueprints(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprints()
-    random_sleep()
+    common.random_sleep()
     assert isinstance(bp_res, list)
     print(f"Blueprint count found in system: '{len(bp_res)}'")
 
 
 def test_get_default_blueprint(admin_session: SandboxRestApiSession):
     bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT)
-    random_sleep()
+    common.random_sleep()
     assert isinstance(bp_res, dict)
     bp_name = bp_res["name"]
     print(f"Pulled details for '{bp_name}'")

From ed7dd8131c22cd0e1071da4b36ade036162bd005 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 22:13:48 +0200
Subject: [PATCH 41/47] readme update

---
 README.MD | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/README.MD b/README.MD
index a5fcc8e..35d52d4 100644
--- a/README.MD
+++ b/README.MD
@@ -17,10 +17,13 @@ pip install cloudshell-sandbox-rest
 ### Basic Usage
 ```python
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
+
+
 api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="Global")
 start_response = api.start_sandbox(blueprint_id="<MY_BLUEPRINT_NAME>", sandbox_name="My Rest Api Blueprint")
 sandbox_id = start_response["id"]
 components_response = api.get_sandbox_components(sandbox_id)
+
 print(f"total components in sandbox: {len(components_response)}")
 ```
 

From 662897459c2ccc0c3283b2203fc6295d5c875de5 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 22:33:52 +0200
Subject: [PATCH 42/47] updated readme

---
 README.MD | 67 +++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 53 insertions(+), 14 deletions(-)

diff --git a/README.MD b/README.MD
index 35d52d4..83902ab 100644
--- a/README.MD
+++ b/README.MD
@@ -2,11 +2,13 @@
 [![PyPI version](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper.svg)](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper)
 
 # Cloudshell Sandbox Rest Api Client
-A python client wrapper around the [cloudshell sandbox api](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/CS-Snbx-API-Topic.htm?Highlight=sandbox%20api).
-This client provides an object-oriented interface to instantiating an api session and interacting with the endpoints.
 
-No additional library object wrapping implemented. All methods return the json.loads python object in the shape of the documented json response. See the [documentation](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/RefGuides/Sndbx-REST-API/REST-API-V2-Ref-Guide.htm?tocpath=CloudShell%20API%20Guide%7CCloudShell%20Sandbox%20API%7C_____3) 
-for details. 
+A python client wrapper around
+the [cloudshell sandbox api](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/CS-Snbx-API-Topic.htm?Highlight=sandbox%20api)
+. No additional library object wrapping implemented. All methods return the json.loads python object in the shape of the
+documented json response. See
+the [documentation](https://help.quali.com/Online%20Help/0.0/Portal/Content/API/RefGuides/Sndbx-REST-API/REST-API-V2-Ref-Guide.htm?tocpath=CloudShell%20API%20Guide%7CCloudShell%20Sandbox%20API%7C_____3)
+for details.
 
 ### Installation
 
@@ -15,12 +17,21 @@ pip install cloudshell-sandbox-rest
 ```
 
 ### Basic Usage
+
 ```python
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
+# pull in api user credentials
+CS_SERVER = "localhost"
+CS_USER = "admin"
+CS_PASSWORD = "admin"
+CS_DOMAIN = "Global"
+
+TARGET_BLUEPRINT = "<MY_BLUEPRINT_NAME>"
+DEPLOYED_SANDBOX_NAME = "My Rest Api Blueprint"
 
-api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="Global")
-start_response = api.start_sandbox(blueprint_id="<MY_BLUEPRINT_NAME>", sandbox_name="My Rest Api Blueprint")
+api = SandboxRestApiSession(host=CS_SERVER, username=CS_USER, password=CS_PASSWORD, domain=CS_DOMAIN)
+start_response = api.start_sandbox(blueprint_id=TARGET_BLUEPRINT, sandbox_name=DEPLOYED_SANDBOX_NAME)
 sandbox_id = start_response["id"]
 components_response = api.get_sandbox_components(sandbox_id)
 
@@ -28,15 +39,24 @@ print(f"total components in sandbox: {len(components_response)}")
 ```
 
 ### Context Manager Usage
+
 Using the api session with a context manager "with" statement will log out and invalidate the token for you.
 
 ```python
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
-api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="Global")
+CS_SERVER = "localhost"
+CS_USER = "admin"
+CS_PASSWORD = "admin"
+CS_DOMAIN = "Global"
+
+TARGET_BLUEPRINT = "<MY_BLUEPRINT_NAME>"
+DEPLOYED_SANDBOX_NAME = "My Rest Api Blueprint"
+
+api = SandboxRestApiSession(host=CS_SERVER, username=CS_USER, password=CS_PASSWORD, domain=CS_DOMAIN)
 
 with api:
-    start_response = api.start_sandbox(blueprint_id="<MY_BLUEPRINT_NAME>", sandbox_name="My Rest Api Blueprint")
+    start_response = api.start_sandbox(blueprint_id=TARGET_BLUEPRINT, sandbox_name=DEPLOYED_SANDBOX_NAME)
     sandbox_id = start_response["id"]
     components_response = api.get_sandbox_components(sandbox_id)
     print(f"total components in sandbox: {len(components_response)}")
@@ -44,26 +64,45 @@ with api:
 
 print(f"session token outside context manager: {api.token}")
 ```
+
 - NOTE: api login happens during init, not on entering context
 - context exit invalidates token
 
 ### Instantiate Session with Token
-Common use case is for admin to pull user token and start a session on their behalf. This can be done as seen in example below.
+
+Common use case is for admin to pull user token and start a session on their behalf. This can be done as seen in example
+below.
+
 ```python
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
-admin_api = SandboxRestApiSession(host="localhost", username="admin", password="admin", domain="Global")
+# admin credentials
+CS_SERVER = "localhost"
+CS_USER = "admin"
+CS_PASSWORD = "admin"
+ADMIN_DOMAIN = "Global"
+
+# end user details
+TARGET_END_USER = "end user"
+TARGET_USER_DOMAIN = "<END_USERS_DOMAIN>"
+
+TARGET_BLUEPRINT = "<MY_BLUEPRINT_NAME>"
+DEPLOYED_SANDBOX_NAME = "My Rest Api Blueprint"
+
+admin_api = SandboxRestApiSession(host=CS_SERVER, username=CS_USER, password=CS_PASSWORD, domain=ADMIN_DOMAIN)
 
 with admin_api:
-    user_token = admin_api.get_token_for_target_user("end_user")
-    user_api = SandboxRestApiSession(host="localhost", token=user_token, domain="<END_USERS_DOMAIN>")
+    user_token = admin_api.get_token_for_target_user(TARGET_END_USER)
+    user_api = SandboxRestApiSession(host=CS_SERVER, token=user_token, domain=TARGET_USER_DOMAIN)
     with user_api:
-        start_response = user_api.start_sandbox(blueprint_id="<MY_BLUEPRINT_NAME>", sandbox_name="My Rest Api Blueprint")
+        start_response = user_api.start_sandbox(blueprint_id=TARGET_BLUEPRINT, sandbox_name=DEPLOYED_SANDBOX_NAME)
         sandbox_id = start_response["id"]
         components_response = user_api.get_sandbox_components(sandbox_id)
         print(f"total components in sandbox: {len(components_response)}")
 ```
+
 - Note the use of nested context managers to manage the different session tokens
 
-### License 
+### License
+
 Free Software: MIT License
\ No newline at end of file

From 0363e1d4b0eaf52c1df6dbf5f50447d5b7168028 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 23:06:52 +0200
Subject: [PATCH 43/47] updated workflow

---
 .github/workflows/lint-test.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml
index a784600..35c1c91 100644
--- a/.github/workflows/lint-test.yml
+++ b/.github/workflows/lint-test.yml
@@ -1,4 +1,4 @@
-name: Pre-commit Lint
+name: Lint and Test
 
 on:
   pull_request:

From 79759ed240303c4fc3623f7135bb9f6f484743c0 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Sun, 9 Jan 2022 23:10:27 +0200
Subject: [PATCH 44/47] add badge

---
 README.MD | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.MD b/README.MD
index 83902ab..6121b7b 100644
--- a/README.MD
+++ b/README.MD
@@ -1,5 +1,5 @@
 [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python/)
-[![PyPI version](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper.svg)](https://badge.fury.io/py/cloudshell-sandboxapi-wrapper)
+[![Lint and Test](https://github.com/QualiSystemsLab/Sandbox-API-Python/actions/workflows/lint-test.yml/badge.svg)](https://github.com/QualiSystemsLab/Sandbox-API-Python/actions/workflows/lint-test.yml)
 
 # Cloudshell Sandbox Rest Api Client
 

From b62bfd7fe5b1f53f53137a71e9a73f23240183bf Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Mon, 10 Jan 2022 00:25:56 +0200
Subject: [PATCH 45/47] added pypi badge

---
 README.MD | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.MD b/README.MD
index 6121b7b..ee42b77 100644
--- a/README.MD
+++ b/README.MD
@@ -1,5 +1,6 @@
 [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python/)
 [![Lint and Test](https://github.com/QualiSystemsLab/Sandbox-API-Python/actions/workflows/lint-test.yml/badge.svg)](https://github.com/QualiSystemsLab/Sandbox-API-Python/actions/workflows/lint-test.yml)
+[![PyPI version](https://badge.fury.io/py/cloudshell-sandbox-rest.svg)](https://badge.fury.io/py/cloudshell-sandbox-rest)
 
 # Cloudshell Sandbox Rest Api Client
 

From a8990f2c02bcd9138308c5f4916cf0b37a7a00f8 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Tue, 11 Jan 2022 20:29:35 +0200
Subject: [PATCH 46/47] fixed token bug

---
 src/cloudshell/sandbox_rest/sandbox_api.py |  7 ++++---
 tests/conftest.py                          |  6 ++++--
 tests/test_api_no_sandbox.py               | 17 +++++++++++------
 3 files changed, 19 insertions(+), 11 deletions(-)

diff --git a/src/cloudshell/sandbox_rest/sandbox_api.py b/src/cloudshell/sandbox_rest/sandbox_api.py
index 203b9fa..63a4970 100644
--- a/src/cloudshell/sandbox_rest/sandbox_api.py
+++ b/src/cloudshell/sandbox_rest/sandbox_api.py
@@ -47,6 +47,7 @@ def __init__(
         self.login()
 
     def login(self, user="", password="", token="", domain="") -> None:
+        """ Called from init - can also be used to refresh session with credentials """
         self.user = user or self.user
         self.password = password or self.password
         self.token = token or self.token
@@ -55,10 +56,10 @@ def login(self, user="", password="", token="", domain="") -> None:
         if not self.domain:
             raise ValueError("Domain must be passed to login")
 
-        if not self.token and self.user and self.password:
+        if not self.token:
+            if not self.user or not self.password:
+                raise ValueError("Login requires Token or Username / Password")
             self.token = self._get_token_with_credentials(self.user, self.password, self.domain)
-        else:
-            raise ValueError("Login requires Token or Username / Password")
 
         self._set_auth_header_on_session()
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 0bbf68f..2a1dbf4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,11 +12,13 @@ def admin_session() -> SandboxRestApiSession:
     admin_api = SandboxRestApiSession(
         host=CLOUDSHELL_SERVER, username=CLOUDSHELL_ADMIN_USER, password=CLOUDSHELL_ADMIN_PASSWORD, domain=CLOUDSHELL_DOMAIN
     )
+    print(f"Admin session started. Token: {admin_api.token}")
     with admin_api:
         yield admin_api
         time.sleep(3)
-        print("admin session token revoked")
-        print(f"total requests: {admin_api.rest_service.request_counter}")
+
+    print("admin session token revoked")
+    print(f"total requests: {admin_api.rest_service.request_counter}")
 
 
 @pytest.fixture(scope="session")
diff --git a/tests/test_api_no_sandbox.py b/tests/test_api_no_sandbox.py
index 2a80487..521d024 100644
--- a/tests/test_api_no_sandbox.py
+++ b/tests/test_api_no_sandbox.py
@@ -3,8 +3,9 @@
 Live Cloudshell server is still a dependency
 """
 import common
+import constants
+import env_settings
 import pytest
-from constants import DEFAULT_EMPTY_BLUEPRINT
 
 from cloudshell.sandbox_rest.sandbox_api import SandboxRestApiSession
 
@@ -12,13 +13,17 @@
 @pytest.fixture(scope="module")
 def api_token(admin_session: SandboxRestApiSession):
     token = admin_session.get_token_for_target_user("admin")
+    print(f"Get User token response: '{token}'")
     return token
 
 
-def test_delete_token(admin_session: SandboxRestApiSession, api_token: str):
-    assert isinstance(api_token, str)
-    print(f"Token response: '{api_token}'")
-    admin_session.delete_token(api_token)
+def test_login_with_token(api_token):
+    new_api = SandboxRestApiSession(host=env_settings.CLOUDSHELL_SERVER, token=api_token)
+    print(f"\nnew api token: {new_api.token}")
+    sandbox_res = new_api.get_sandboxes()
+    assert isinstance(sandbox_res, list)
+    new_api.delete_token(new_api.token)
+    print("new api token deleted")
 
 
 def test_get_sandboxes(admin_session: SandboxRestApiSession):
@@ -36,7 +41,7 @@ def test_get_blueprints(admin_session: SandboxRestApiSession):
 
 
 def test_get_default_blueprint(admin_session: SandboxRestApiSession):
-    bp_res = admin_session.get_blueprint_details(DEFAULT_EMPTY_BLUEPRINT)
+    bp_res = admin_session.get_blueprint_details(constants.DEFAULT_EMPTY_BLUEPRINT)
     common.random_sleep()
     assert isinstance(bp_res, dict)
     bp_name = bp_res["name"]

From 1e5027a4a4fe87a763379d21d1ce48393a6ad188 Mon Sep 17 00:00:00 2001
From: katzy687 <katzy687@gmail.com>
Date: Wed, 12 Jan 2022 00:00:45 +0200
Subject: [PATCH 47/47] bump version

---
 version.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/version.txt b/version.txt
index 6c6aa7c..6da28dd 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.1.0
\ No newline at end of file
+0.1.1
\ No newline at end of file
