Commit 0f10ffbe authored by Mark Hewitt's avatar Mark Hewitt
Browse files

user friendly output

parent c79fee75
Pipeline #71326 passed with stages
in 1 minute and 57 seconds
......@@ -4,7 +4,6 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
......@@ -13,11 +12,11 @@
"console": "integratedTerminal"
},
{
"name": "Pytest: Current File",
"name": "Python: Module CmdVar",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal"
"console": "integratedTerminal",
"module": "${command:extension.commandvariable.file.relativeDirDots}.${fileBasenameNoExtension}",
},
{
"name": "Python: osdu config list",
......
......@@ -42,6 +42,14 @@ class State: # pylint: disable=too-few-public-methods
def __repr__(self):
return f"State=Debug: {self.debug}, Config path: {self.config_path}"
def is_user_friendly_mode(self) -> bool:
"""Return whether we are running in a user friendly output mode
Returns:
bool: whether running in user friendly mode
"""
return self.output is None
def _format_click_options(cmd: click.Command, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes options into the formatter split between Options and Common Options."""
......
......@@ -148,35 +148,23 @@ class CliOsduClient(OsduClient):
ok_status_codes (list, optional): Status codes for successful call. Defaults to [200].
"""
url = self._url_from_config(config_url_key, url_extra_path)
if ok_status_codes is None:
ok_status_codes = [200]
response = self.get(url)
if response.status_code not in ok_status_codes:
if ok_status_codes is not None and response.status_code not in ok_status_codes:
raise HTTPError(response=response)
return response
def cli_get_returning_json(self, config_url_key: str, url_extra_path: str) -> dict:
def cli_get_returning_json(
self, config_url_key: str, url_extra_path: str, ok_status_codes: list = None
) -> dict:
"""[summary]
Args:
config_url_key (str): key in configuration for the base path
url_extra_path (str): extra path to add to the base path
ok_status_codes (list, optional): accepted ok response codes. Defaults to None.
"""
response = self.cli_get(config_url_key, url_extra_path)
try:
return response.json()
except HTTPError as ex:
logger.error(MSG_HTTP_ERROR)
logger.error("Error (%s) - %s", ex.response.status_code, ex.response.reason)
except ValueError as ex:
logger.error(MSG_JSON_DECODE_ERROR)
logger.debug(ex)
except (NoOptionError, NoSectionError) as ex:
logger.warning(
"Configuration missing from config ('%s'). Run 'osdu config update'", ex.args[0]
)
sys.exit(1)
url = self._url_from_config(config_url_key, url_extra_path)
return self.get_returning_json(url, ok_status_codes)
def cli_post_returning_json(
self,
......@@ -184,7 +172,7 @@ class CliOsduClient(OsduClient):
url_extra_path: str,
data: Union[str, dict],
ok_status_codes: list = None,
):
) -> dict:
"""[summary]
Args:
......@@ -194,24 +182,10 @@ class CliOsduClient(OsduClient):
ok_status_codes (list, optional): Status codes indicating successful call. Defaults to [200].
Returns:
[type]: [description]
dict: returned json
"""
try:
url = self._url_from_config(config_url_key, url_extra_path)
return self.post_returning_json(url, data, ok_status_codes)
except HTTPError as ex:
logger.error(MSG_HTTP_ERROR)
logger.debug(ex.response.text)
logger.error("Error (%s) - %s", ex.response.status_code, ex.response.reason)
except ValueError as ex:
logger.error(MSG_JSON_DECODE_ERROR)
logger.debug(ex)
except (NoOptionError, NoSectionError) as ex:
logger.warning(
"Configuration missing from config ('%s'). Run 'osdu config update'", ex.args[0]
)
sys.exit(1)
url = self._url_from_config(config_url_key, url_extra_path)
return self.post_returning_json(url, data, ok_status_codes)
def cli_delete(
self,
......@@ -247,35 +221,6 @@ class CliOsduClient(OsduClient):
sys.exit(1)
# # loop for implementing retries send process
# retries = config.getint("CONNECTION", "retries")
# for retry in range(retries):
# try:
# if response.status_code in DATA_LOAD_OK_RESPONSE_CODES:
# workflow_response = response.json()
# logger.info(f"Response: {workflow_response}")
# file_logger.info(f"{workflow_response.get('runId')}")
# break
# reason = response.text[:250]
# logger.error(f"Request error.")
# logger.error(f"Response status: {response.status_code}. "
# f"Response content: {reason}.")
# if retry + 1 < retries:
# if response.status_code in BAD_TOKEN_RESPONSE_CODES:
# logger.error(f"Error in Request: {headers.get('correlation-id')})")
# else:
# time_to_sleep = config.getint("CONNECTION", "timeout")
# logger.info(f"Retrying in {time_to_sleep} seconds...")
# time.sleep(time_to_sleep)
# except (requests.RequestException, HTTPError) as exc:
# logger.error(f"Unexpected request error. Reason: {exc}")
# sys.exit(2)
def cli_put(
self,
config_url_key: str,
......
......@@ -13,9 +13,9 @@
"""Code to handle status commands"""
import click
from requests.models import HTTPError
from requests.exceptions import RequestException
from osducli.click_cli import CustomClickCommand, global_params
from osducli.click_cli import CustomClickCommand, State, command_with_output
from osducli.cliclient import CliOsduClient, handle_cli_exceptions
from osducli.config import (
CONFIG_FILE_URL,
......@@ -26,40 +26,58 @@ from osducli.config import (
CONFIG_UNIT_URL,
CONFIG_WORKFLOW_URL,
)
from osducli.log import get_logger
logger = get_logger(__name__)
@click.command(cls=CustomClickCommand)
@handle_cli_exceptions
@global_params
def _click_command(state):
@command_with_output("results[]")
def _click_command(state: State):
# def _click_command(ctx, debug, config, hostname):
"""Shows the status of OSDU services"""
status(state)
return status(state)
def status(state): # pylint: disable=unused-argument
def status(state: State): # pylint: disable=unused-argument
"""status command entry point
User friendly mode displays results as received for responsiveness.
Args:
state (State): Global state
"""
connection = CliOsduClient(state.config)
# HTTPConnection.debuglevel = 1
check_print_status(connection, "File service", CONFIG_FILE_URL, "readiness_check")
check_print_status(connection, "Legal service", CONFIG_LEGAL_URL, "_ah/readiness_check")
check_print_status(connection, "Schema service", CONFIG_SCHEMA_URL, "schema?limit=1")
check_print_status(connection, "Search service", CONFIG_SEARCH_URL, "health/readiness_check")
check_print_status(connection, "Storage service", CONFIG_STORAGE_URL, "health")
check_print_status(connection, "Unit service", CONFIG_UNIT_URL, "../_ah/readiness_check")
check_print_status(connection, "Workflow service", CONFIG_WORKFLOW_URL, "../readiness_check")
results = []
services = [
("File service", CONFIG_FILE_URL, "readiness_check"),
("Legal service", CONFIG_LEGAL_URL, "_ah/readiness_check"),
("Schema service", CONFIG_SCHEMA_URL, "schema?limit=1"),
("Search service", CONFIG_SEARCH_URL, "health/readiness_check"),
("Storage service", CONFIG_STORAGE_URL, "health"),
("Unit service", CONFIG_UNIT_URL, "../_ah/readiness_check"),
("Workflow service", CONFIG_WORKFLOW_URL, "../readiness_check"),
]
for service in services:
result = _check_status(connection, service[0], service[1], service[2])
results.append(result)
if state.is_user_friendly_mode():
print(f"{result['name'].ljust(20)} {result['status']}\t {result['reason']}")
return None if state.is_user_friendly_mode() else {"results": results}
def check_print_status(
connection: CliOsduClient, name: str, config_url_key: str, url_extra_path: str
):
"""Check the status of the given service and print information"""
def _check_status(connection: CliOsduClient, name: str, config_url_key: str, url_extra_path: str):
"""Check the status of the given service"""
try:
response = connection.cli_get(config_url_key, url_extra_path)
print(f"{name.ljust(20)} {response.status_code}\t {response.reason}")
except (HTTPError) as ex:
print(f"{name.ljust(20)} {ex.response.status_code}\t {ex.response.reason}")
_status = response.status_code
_reason = response.reason
except RequestException as _ex: # pylint: disable=broad-except
exception_message = str(_ex) if len(str(_ex)) > 0 else "Unknown Error"
logger.debug(exception_message)
_status = _ex.response.status_code if _ex.response else -1
_reason = _ex.response.reason if _ex.response else exception_message
result = {"name": name, "status": _status, "reason": _reason}
return result
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for OSDU CLI"""
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test cases for click_cli State"""
import logging
import unittest
from mock import MagicMock, PropertyMock, patch
from nose2.tools import params
from requests.exceptions import RequestException
from testfixtures import LogCapture
from osducli.click_cli import State
from osducli.commands.status.status import status
from ...helpers import MockConfig
class StatusTests(unittest.TestCase):
"""Test cases for status comand"""
def test_with_output(self):
"""Test json output"""
response_mock = MagicMock()
type(response_mock).status_code = PropertyMock(return_value=200)
type(response_mock).reason = PropertyMock(return_value="Some Reason")
with patch("osducli.cliclient.CliOsduClient.cli_get", return_value=response_mock) as _:
state = State()
state.config = MockConfig
state.output = "json"
results = status(state)
self.assertEqual(len(results["results"]), 7) # Return json with 7 entries
for result in results["results"]:
self.assertIsNotNone(result["name"])
self.assertEqual(result["status"], 200)
self.assertEqual(result["reason"], "Some Reason")
def test_user_friendly_output(self):
"""Test user friendly output"""
response_mock = MagicMock()
type(response_mock).status_code = PropertyMock(return_value=200)
type(response_mock).reason = PropertyMock(return_value="Some Reason")
with patch("osducli.cliclient.CliOsduClient.cli_get", return_value=response_mock) as _:
state = State()
state.config = MockConfig
results = status(state)
self.assertIsNone(results) # Don't return any results
# TO DO - Verify stdout.
def test_http_call_count(self):
"""Test http call count"""
response_mock = MagicMock()
type(response_mock).status_code = PropertyMock(return_value=200)
type(response_mock).reason = PropertyMock(return_value="Some Reason")
with patch(
"osducli.cliclient.CliOsduClient.cli_get", return_value=response_mock
) as cli_get_patch:
state = State()
state.config = MockConfig
status(state)
self.assertEqual(cli_get_patch.call_count, 7) # Backend called 7 times
@params(
(404, ""),
(500, ""),
)
def test_request_error_with_response(self, status_code, reason):
"""Test RequestExceptions with a response are handled ok"""
response_mock = MagicMock()
type(response_mock).status_code = PropertyMock(return_value=status_code)
type(response_mock).reason = PropertyMock(return_value=reason)
with patch(
"osducli.cliclient.CliOsduClient.cli_get",
side_effect=RequestException(response=response_mock),
) as _:
state = State()
state.config = MockConfig
state.output = "json"
with LogCapture(level=logging.DEBUG) as log_capture:
results = status(state)
self.assertEqual(len(results["results"]), 7) # Return json with 7 entries
self.assertEqual(len(log_capture.records), 7) # 7 debug log records
for result in results["results"]:
self.assertIsNotNone(result["name"])
self.assertEqual(result["status"], status_code)
self.assertEqual(result["reason"], reason)
def test_request_error_no_response(self):
"""Test RequestExceptions without a response are handled ok"""
_exception = RequestException()
with patch(
"osducli.cliclient.CliOsduClient.cli_get",
side_effect=_exception,
) as _:
state = State()
state.config = MockConfig
state.output = "json"
with LogCapture(level=logging.DEBUG) as log_capture:
results = status(state)
print(log_capture)
self.assertEqual(len(results["results"]), 7) # Return json with 7 entries
self.assertEqual(len(log_capture.records), 7) # 7 debug log records
for result in results["results"]:
self.assertIsNotNone(result["name"])
self.assertEqual(result["status"], -1)
self.assertEqual(result["reason"], "Unknown Error")
if __name__ == "__main__":
import nose2
nose2.main()
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test cases for click_cli State"""
import unittest
from osducli.click_cli import State
class ClickCliStateTests(unittest.TestCase):
"""Test cases for test_click_cli State"""
def test_init(self):
"""Test the init method"""
state = State()
self.assertEqual(state.debug, False)
self.assertEqual(state.config_path, None)
self.assertEqual(state.config, None)
self.assertEqual(state.output, None)
self.assertEqual(state.jmes, None)
def test_user_friendly_mode(self):
"""Test the init method"""
state = State()
user_friendly = state.is_user_friendly_mode()
self.assertTrue(user_friendly)
def test_user_friendly_mode_output_set(self):
"""Test the init method"""
state = State()
state.output = "json"
user_friendly = state.is_user_friendly_mode()
self.assertFalse(user_friendly)
if __name__ == "__main__":
import nose2
nose2.main()
......@@ -76,18 +76,71 @@ class CliOsduClientTests(ScenarioTest):
with self.assertRaises(SystemExit):
CliOsduClient(MOCK_CONFIG_INVALID_AUTH)
# """Playground test for unit commands - some notes / examples"""
# @patch('osducli.commands.unit.custom.get_as_json', side_effect=ValueError('ValueError'))
# @patch('osducli.commands.unit.custom.get_url_as_json', autospec=True, return_value=(not_found_response_mock,None))
#
# @patch('osducli.commands.unit.custom.get_headers')
# def test_unit_list(self, test_patch):
# region test cli_get
@params(
("config", "/path"),
("config", "/path1"),
("config2", "path2"),
)
def test_cli_get(self, config, path):
"""Test valid get with string returns expected values"""
response_mock = Mock()
with patch.object(OsduClient, "get", return_value=response_mock) as mock_get:
client = CliOsduClient(MockConfig)
response = client.cli_get(config, path)
mock_get.assert_called_once()
mock_get.assert_called_with("https://dummy.com/core_" + config + path)
self.assertEqual(response_mock, response)
def test_cli_get_defaults(self):
"""Test valid get with string returns expected values"""
response_mock = Mock()
with patch.object(OsduClient, "get", return_value=response_mock) as mock_get:
client = CliOsduClient(MockConfig)
response = client.cli_get("config", "/path")
mock_get.assert_called_once()
mock_get.assert_called_with("https://dummy.com/core_config/path")
self.assertEqual(response_mock, response)
@params(
(None, 200), # No status codes passed then all should be ok
(None, 404), # No status codes passed then all should be ok
([200], 200),
([200, 202], 202),
([202], 202),
)
def test_cli_get_status_codes(self, ok_status_codes, actual_status_code):
"""Test valid get returns expected values"""
response_mock = Mock()
type(response_mock).status_code = PropertyMock(return_value=actual_status_code)
with patch.object(OsduClient, "get", return_value=response_mock) as mock_get:
client = CliOsduClient(MockConfig)
response = client.cli_get("config", "/path", ok_status_codes)
mock_get.assert_called_once()
self.assertEqual(response_mock, response)
@params(
([], 200), # Empty list means everything should fail
([200], 404),
([200, 202], 500),
)
def test_get_http_error_throws_exception(self, ok_status_codes, actual_status_code):
"""Test get error returns expected values"""
response_mock = MagicMock()
type(response_mock).status_code = PropertyMock(return_value=actual_status_code)
with patch.object(OsduClient, "get", return_value=response_mock) as _:
with self.assertRaises(HTTPError):
client = CliOsduClient(MockConfig)
# # with patch('app.mocking.get_user_name', return_value = 'Mocked This Silly'):
# # ret = test_method()
# # self.assertEqual(ret, 'Mocked This Silly')
client.cli_get("config", "/path", ok_status_codes)
# unit_list()
# endregion test cli_get
# If doing a new live test to get / refresh a recording then comment out the below patch and
# after getting a recording delete any recording authentication interactions
......@@ -114,12 +167,12 @@ class CliOsduClientTests(ScenarioTest):
@patch.object(CliOsduClient, "get", return_value=not_found_response_mock)
def test_cli_osdu_connection_cli_get_returning_json_404(self, mock_url_from_config, mock_get):
"""Test 404 errors return the correct message"""
with self.assertRaises(HTTPError) as sysexit:
with self.assertRaises(HTTPError) as _ex:
with LogCapture(level=logging.INFO) as log_capture:
connection = CliOsduClient(MockConfig)
_ = connection.cli_get_returning_json("DUMMY_KEY", "DUMMY_STRING")
log_capture.check_present(("cli", "ERROR", MSG_HTTP_ERROR))
self.assertEqual(sysexit.exception.code, 1)
self.assertEqual(_ex.exception.code, 1)
# # pylint: disable=W0613
# @patch.object(CliOsduClient, '_url_from_config', return_value='https://www.test.com/test')
......@@ -141,6 +194,47 @@ class CliOsduClientTests(ScenarioTest):
"""Return true always (only 1 query)."""
return True
# region test cli_post_returning_json
@params(
("config", "/path", "string1", None),
("config", "/path1", "string1", None),
("config", "/path2", "string1", None),
("config", "/path2", "string2", None),
("config2", "path2", "string2", None),
("config2", "path2", "string2", [200]),
)
def test_cli_post_returning_json(self, config, path, string_data, status_codes):
"""Test valid post with string returns expected values"""
response_mock = Mock()
with patch.object(
OsduClient, "post_returning_json", return_value=response_mock
) as mock_post:
client = CliOsduClient(MockConfig)
response = client.cli_post_returning_json(config, path, string_data, status_codes)
mock_post.assert_called_once()
mock_post.assert_called_with(
"https://dummy.com/core_" + config + path, string_data, status_codes
)
self.assertEqual(response_mock, response)
def test_cli_post_returning_json_defaults(self):
"""Test valid post with string returns expected values"""
response_mock = Mock()
with patch.object(
OsduClient, "post_returning_json", return_value=response_mock
) as mock_post:
client = CliOsduClient(MockConfig)
response = client.cli_post_returning_json("config", "/path", "data")
mock_post.assert_called_once()
mock_post.assert_called_with("https://dummy.com/core_config/path", "data", None)
self.assertEqual(response_mock, response)
# endregion test cli_post_returning_json
# region test cli_put
@params(
("config", "/path", "string1"),
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment