shipyard/shipyard_client/cli/format_utils.py
Bryan Strassner db5f22d34e [391107] Sorting of commit configdocs results
Sorts Errors > Warnings > Info in results returned. Adds the
source of the error if known.

Change-Id: I2d449bc4f999909fe84b10686fdb8e3c7d5fcd5f
2018-03-23 14:14:11 -04:00

212 lines
6.9 KiB
Python

# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# 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.
import json
import yaml
from prettytable import PrettyTable
from prettytable.prettytable import PLAIN_COLUMNS
_INDENT = ' ' * 8
def cli_format_error_handler(response):
"""Generic handler for standard Shipyard error responses
Method is intended for use when there is no special handling needed
for the response.
:param client_response: a requests response object assuming the
standard error format
:returns: a generically formatted error response formulated from the
client_repsonse. The response will be in the format:
Error: {{message}}
Reason: {{Reason}}
Additional: {{details message list messages}}
...
"""
return cli_format_status_handler(response, is_error=True)
def cli_format_status_handler(response, is_error=False):
"""Handler for standard Shipyard status and status-based error responses
Method is intended for use when there is no special handling needed
for the response. If the response is empty, returns empty string
:param client_response: a requests response object assuming the
standard error format
:is_error: toggles the use of status or error verbiage
:returns: a generically formatted error response formulated from the
client_repsonse. The response will be in the format:
[Error|Status]: {{message}}
Reason: {{Reason}}
Additional: {{details message list messages}}
...
"""
formatted = "Error: {}\nReason: {}" if is_error \
else "Status: {}\nReason: {}"
try:
if response.text:
resp_j = response.json()
resp = formatted.format(resp_j.get('message', 'Not specified'),
resp_j.get('reason', 'Not specified'))
if resp_j.get('details'):
mlist = resp_j['details'].get('messageList', [])
for message in sorted(mlist,
key=lambda m: _lvl_key(
m.get('level'),
m.get('error', False))):
if message.get('kind') == 'ValidationMessage':
resp = resp + _format_validation_message(message)
else:
resp = resp + _format_basic_message(message)
if message.get('source'):
resp = resp + "\n{}Source: {}".format(
_INDENT,
message['source']
)
return resp
else:
return ''
except ValueError:
return "Error: Unable to decode response. Value: {}".format(
response.text)
# Map of levels by severity. Extra values are included in this map but valid
# values are defined here:
# https://github.com/att-comdev/ucp-integration/blob/master/docs/source/api-conventions.rst#validationmessage-message-type
_LEVEL_KEYS = {
0: ['error', 'fatal'],
1: ['warn', 'warning'],
2: ['info', 'debug'],
}
_SENTINEL_LEVEL = "999"
def _lvl_key(level_name, error):
"""Generate a level key value
Returns a value to support sort order based on lvls dict.
The result is that like-level items are sorted together.
"""
if level_name is None:
if (error):
level_name = 'error'
else:
level_name = 'info'
else:
level_name = level_name.lower()
for key, val_list in _LEVEL_KEYS.items():
if level_name in val_list:
return '{}{}'.format(key, level_name)
return _SENTINEL_LEVEL
def _format_validation_message(message):
"""Formats a ValidationMessage
Returns a single string with embedded newlines
"""
resp = '\n- {}: {}'.format(message.get('level'), message.get('name'))
resp = resp + '\n{}Message: {}'.format(_INDENT, message.get('message'))
if message.get('diagnostic'):
resp = resp + '\n{}Diagnostic: {}'.format(
_INDENT, message.get('diagnostic')
)
for doc in message.get('documents', []):
resp = resp + '\n{}Document: {} - {}'.format(
_INDENT,
doc.get('schema'),
doc.get('name')
)
return resp
def _format_basic_message(message):
"""Formats a basic message
Returns a single string with embedded newlines
"""
if message.get('error', False):
resp = '\n- Error: {}'.format(message.get('message'))
else:
resp = '\n- Info: {}'.format(message.get('message'))
return resp
def raw_format_response_handler(response):
"""Basic format handler to return raw response text"""
return response.text
def formatted_response_handler(response):
"""Base format handler for either json or yaml depending on call"""
call = response.headers['Content-Type']
if 'json' in call:
try:
return json.dumps(response.json(), sort_keys=True, indent=4)
except ValueError:
return (
"This is not json and could not be printed as such. \n" +
response.text
)
else: # all others should be yaml
try:
return (yaml.dump_all(
yaml.safe_load_all(response.content),
width=79,
indent=4,
default_flow_style=False))
except ValueError:
return (
"This is not yaml and could not be printed as such.\n" +
response.text
)
def table_factory(field_names=None, rows=None, style=None):
"""Generate a table using prettytable
Factory method for a prettytable using the PLAIN_COLUMN style unless
ovrridden by the style parameter.
If a field_names list of strings is passed in, the column names
will be initialized.
If rows are supplied (list of lists), the will be added as rows in
order.
"""
p = PrettyTable()
if style is None:
p.set_style(PLAIN_COLUMNS)
else:
p.set_style(style)
if field_names:
p.field_names = field_names
else:
p.header = False
if rows:
for row in rows:
p.add_row(row)
# This alignment only works if columns and rows are set up.
p.align = 'l'
return p
def table_get_string(table, align='l'):
"""Wrapper to return a prettytable string with the supplied alignment"""
table.align = 'l'
return table.get_string()