diff --git a/README.md b/README.md index 61cb8dd..14363fc 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ usage: tsp-cli-client [-h] [--ip IP] [--port PORT] [--open-trace TRACE_PATH] [--list-experiment UUID] [--list-experiments] [--delete-experiment UUID] [--list-outputs UUID] [--list-output OUTPUT_ID] [--get-tree OUTPUT_ID] + [--get-xy-tree OUTPUT_ID] [--get-xy OUTPUT_ID] + [--items [ITEMS ...]] [--times [TIMES ...]] [--uuid UUID] [--uuids [UUIDS ...]] [--do-delete-traces] [--paths [PATHS ...]] [--list-extensions] [--load-extension EXTENSION_PATH] @@ -83,7 +85,12 @@ optional arguments: --list-outputs UUID Get details on the given trace --list-output OUTPUT_ID Get details on the given output of a trace - --get-tree OUTPUT_ID Get the tree of an output + --get-tree OUTPUT_ID Get the timegraph tree of an output + --get-xy-tree OUTPUT_ID + Get the XY tree of an output + --get-xy OUTPUT_ID Get the XY data of an output + --items [ITEMS ...] The list of XY items requested + --times [TIMES ...] The list of XY times requested --uuid UUID The UUID of a trace --uuids [UUIDS ...] The list of UUIDs --do-delete-traces Also delete traces when deleting experiment diff --git a/test_tsp.py b/test_tsp.py index bf8148d..d9a709a 100644 --- a/test_tsp.py +++ b/test_tsp.py @@ -22,9 +22,17 @@ import os import pytest +import time +from tsp.response import ResponseStatus from tsp.tsp_client import TspClient +REQUESTED_TIME_XY_START = 1332170682440133097 +REQUESTED_TIME_XY_END = 1332170692664579801 +REQUESTED_TIME_XY_LENGTH = 10 +REQUESTED_TIME_XY_STEP = (REQUESTED_TIME_XY_END - + REQUESTED_TIME_XY_START) / REQUESTED_TIME_XY_LENGTH + class TestTspClient: """TspClient test methods. @@ -228,6 +236,62 @@ def test_fetch_timegraph_tree(self, kernel): self._delete_experiments() self._delete_traces() + def test_fetch_xy_tree(self, kernel): + traces = [] + response = self.tsp_client.open_trace(os.path.basename(kernel), kernel) + traces.append(response.model.UUID) + response = self.tsp_client.open_experiment( + os.path.basename(kernel), traces) + assert response.status_code == 200 + experiment_uuid = response.model.UUID + + response = self.tsp_client.fetch_experiment_outputs(experiment_uuid) + output_id = response.model.descriptors[0].id + response = self.tsp_client.fetch_xy_tree(experiment_uuid, output_id) + assert response.status_code == 200 + assert response.model.model_type == response.model.model_type.XY_TREE + self._delete_experiments() + self._delete_traces() + + def test_fetch_xy(self, kernel): + traces = [] + response = self.tsp_client.open_trace(os.path.basename(kernel), kernel) + traces.append(response.model.UUID) + response = self.tsp_client.open_experiment( + os.path.basename(kernel), traces) + assert response.status_code == 200 + experiment_uuid = response.model.UUID + + response = self.tsp_client.fetch_experiment_outputs(experiment_uuid) + output_id = response.model.descriptors[0].id + status = ResponseStatus.RUNNING.name + while status == ResponseStatus.RUNNING.name: + time.sleep(1) + response = self.tsp_client.fetch_xy_tree( + experiment_uuid, output_id) + assert response.model is not None + status = response.model.status.upper() + + parameters = {} + requested_items = [] + for entry in response.model.model.entries: + requested_items.append(entry.id) + parameters[TspClient.REQUESTED_ITEM_KEY] = requested_items + + requested_times = [] + requested_time = REQUESTED_TIME_XY_START + while len(requested_times) < REQUESTED_TIME_XY_LENGTH: + requested_time += REQUESTED_TIME_XY_STEP + requested_times.append(int(requested_time)) + parameters[TspClient.REQUESTED_TIME_KEY] = requested_times + + params = {TspClient.PARAMETERS_KEY: parameters} + response = self.tsp_client.fetch_xy(experiment_uuid, output_id, params) + assert response.status_code == 200 + assert response.model.model_type == response.model.model_type.XY + self._delete_experiments() + self._delete_traces() + def test_fetch_extensions_none(self): response = self.tsp_client.fetch_extensions() assert response.status_code == 200 diff --git a/tsp-cli-client b/tsp-cli-client index 41ee796..5664b66 100755 --- a/tsp-cli-client +++ b/tsp-cli-client @@ -36,6 +36,7 @@ from os.path import os from tsp.tree_model import TreeModel from tsp.tsp_client import TspClient +from tsp.xy_model import XYModel #api_token = 'your_token_goes_here' NS_PER_SEC = 1000000000 @@ -134,7 +135,15 @@ if __name__ == "__main__": parser.add_argument("--list-output", dest="list_output", help="Get details on the given output of a trace", metavar="OUTPUT_ID") parser.add_argument("--get-tree", dest="get_tree", - help="Get the tree of an output", metavar="OUTPUT_ID") + help="Get the timegraph tree of an output", metavar="OUTPUT_ID") + parser.add_argument("--get-xy-tree", dest="get_xy_tree", + help="Get the XY tree of an output", metavar="OUTPUT_ID") + parser.add_argument("--get-xy", dest="get_xy", + help="Get the XY data of an output", metavar="OUTPUT_ID") + parser.add_argument("--items", dest="items", + help="The list of XY items requested", nargs="*") + parser.add_argument("--times", dest="times", + help="The list of XY times requested", nargs="*") parser.add_argument("--uuid", dest="uuid", help="The UUID of a trace") parser.add_argument("--uuids", dest="uuids", help="The list of UUIDs", nargs="*") @@ -298,6 +307,9 @@ if __name__ == "__main__": options.uuid, options.get_tree) if response.status_code == 200: tree = response.model.model + if tree is None: + print("Tree had no model; retry?") + sys.exit(1) if (output_descriptor.type == "DATA_TREE"): treeModel = TreeModel(tree.entries, tree.descriptors) @@ -311,6 +323,56 @@ if __name__ == "__main__": print("Trace UUID is missing") sys.exit(1) + if (options.get_xy_tree): + if options.uuid is not None: + + output_descriptor = get_descriptor( + options.uuid, options.get_xy_tree) + if (output_descriptor is None): + sys.exit(1) + + response = tsp_client.fetch_xy_tree( + options.uuid, options.get_xy_tree) + if response.status_code == 200: + tree = response.model.model + if tree is None: + print("Tree had no model; retry?") + sys.exit(1) + + if (output_descriptor.type == "TREE_TIME_XY"): + treeModel = TreeModel(tree.entries, tree.descriptors) + else: + treeModel = TreeModel(tree.entries) + treeModel.print() + sys.exit(0) + else: + sys.exit(1) + else: + print("Trace UUID is missing") + sys.exit(1) + + if (options.get_xy): + if not options.items or not options.times: + print("Provide requested --items and --times for the XY data") + sys.exit(1) + + if options.uuid is not None: + parameters = {TspClient.REQUESTED_TIME_KEY: list(map(int, options.times)), + TspClient.REQUESTED_ITEM_KEY: list(map(int, options.items))} + params = {TspClient.PARAMETERS_KEY: parameters} + + response = tsp_client.fetch_xy( + options.uuid, options.get_xy, params) + if response.status_code == 200: + xyModel = response.model.model + xyModel.print() + sys.exit(0) + else: + sys.exit(1) + else: + print("Trace UUID is missing") + sys.exit(1) + if (options.extensions): response = tsp_client.fetch_extensions() if response.status_code == 200: diff --git a/tsp/response.py b/tsp/response.py index 1496c8d..ea69623 100644 --- a/tsp/response.py +++ b/tsp/response.py @@ -26,6 +26,7 @@ from tsp.output_descriptor import OutputDescriptor from tsp.entry_model import EntryModel from tsp.time_graph_model import TimeGraphModel +from tsp.xy_model import XYModel MODEL_KEY = "model" OUTPUT_DESCRIPTOR_KEY = "output" @@ -79,14 +80,12 @@ def __init__(self, params, model_type): if self.model_type == ModelType.TIME_GRAPH_TREE: self.model = EntryModel(params.get(MODEL_KEY), self.model_type) elif self.model_type == ModelType.XY_TREE: - # TODO - print("not implemented") + self.model = EntryModel(params.get(MODEL_KEY)) elif self.model_type == ModelType.STATES: # TODO print("not implemented") elif self.model_type == ModelType.XY: - # TODO - print("not implemented") + self.model = XYModel(params.get(MODEL_KEY)) ''' Output descriptor diff --git a/tsp/tsp_client.py b/tsp/tsp_client.py index b402894..a4f8073 100644 --- a/tsp/tsp_client.py +++ b/tsp/tsp_client.py @@ -37,15 +37,16 @@ headers_form = {'content-type': 'application/x-www-form-urlencoded', 'Accept': 'application/json'} -PARAMETERS_KEY = 'parameters' -REQUESTED_TIME_KEY = 'requested_times' - class TspClient(object): ''' Trace Server Protocol tsp_cli_client ''' + PARAMETERS_KEY = 'parameters' + REQUESTED_TIME_KEY = 'requested_times' + REQUESTED_ITEM_KEY = 'requested_items' + def __init__(self, base_url): ''' Constructor @@ -240,9 +241,7 @@ def fetch_timegraph_tree(self, exp_uuid, output_id, parameters=None): params = parameters if (parameters is None): - requested_times = [0, 1] - my_parameters = {REQUESTED_TIME_KEY: requested_times} - params = {PARAMETERS_KEY: my_parameters} + params = {} response = requests.post(api_url, json=params, headers=headers) @@ -252,6 +251,50 @@ def fetch_timegraph_tree(self, exp_uuid, output_id, parameters=None): print("failed to get tree: {0}".format(response.status_code)) return TspClientResponse(None, response.status_code, response.text) + def fetch_xy_tree(self, exp_uuid, output_id, parameters=None): + ''' + Fetch XY tree, Model extends Entry + :param exp_uuid: Experiment UUID + :param output_id: Output ID + :param parameters: Query object + :returns: :class: `TspClientResponse ` object XY entry response with entries and headers + :rtype: TspClientResponse + ''' + api_url = '{0}experiments/{1}/outputs/XY/{2}/tree'.format( + self.base_url, exp_uuid, output_id) + + params = parameters + if (parameters is None): + params = {} + + response = requests.post(api_url, json=params, headers=headers) + + if response.status_code == 200: + return TspClientResponse(GenericResponse(json.loads(response.content.decode('utf-8')), ModelType.XY_TREE), response.status_code, response.text) + else: + print("failed to get tree: {0}".format(response.status_code)) + return TspClientResponse(None, response.status_code, response.text) + + def fetch_xy(self, exp_uuid, output_id, parameters): + ''' + Fetch XY xy, XYModel + :param exp_uuid: Experiment UUID + :param output_id: Output ID + :param parameters: Query object (mandatory here; no defaults possible) + :returns: :class: `TspClientResponse ` object XY series response + :rtype: TspClientResponse + ''' + api_url = '{0}experiments/{1}/outputs/XY/{2}/xy'.format( + self.base_url, exp_uuid, output_id) + + response = requests.post(api_url, json=parameters, headers=headers) + + if response.status_code == 200: + return TspClientResponse(GenericResponse(json.loads(response.content.decode('utf-8')), ModelType.XY), response.status_code, response.text) + else: + print("failed to get xy: {0}".format(response.status_code)) + return TspClientResponse(None, response.status_code, response.text) + def fetch_extensions(self): ''' Fetch Extensions (loaded files) diff --git a/tsp/xy_model.py b/tsp/xy_model.py new file mode 100644 index 0000000..7664c14 --- /dev/null +++ b/tsp/xy_model.py @@ -0,0 +1,214 @@ +# The MIT License (MIT) +# +# Copyright (C) 2021 - Ericsson +# +# 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 NONINFRINGEMENT. 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. + +import copy + +TITLE_KEY = "title" +COMMON_X_AXIS_KEY = "commonXAxis" +SERIES_KEY = "series" +SERIES_NAME_KEY = "seriesName" +SERIES_ID_KEY = "seriesId" +X_AXIS_KEY = "xAxis" +Y_AXIS_KEY = "yAxis" +X_VALUES_KEY = "xValues" +Y_VALUES_KEY = "yValues" +TAGS_KEY = "tags" +LABEL_KEY = "label" +UNIT_KEY = "unit" +DATA_TYPE_KEY = "dataType" + + +class XYModel(object): + ''' + Model of a XY chart, contains at least one XY series + ''' + + def __init__(self, params): + ''' + Title of the model + ''' + if TITLE_KEY in params: + self.title = params.get(TITLE_KEY) + del params[TITLE_KEY] + + ''' + Indicate if all the Y values are using the same X axis + ''' + if COMMON_X_AXIS_KEY in params: + self.common_x_axis = params.get(COMMON_X_AXIS_KEY) + del params[COMMON_X_AXIS_KEY] + + ''' + Array of XY series + ''' + self.series = [] + if SERIES_KEY in params: + for series in params.get(SERIES_KEY): + self.series.append(XYSeries(series)) + del params[SERIES_KEY] + + ''' + Store other key/value pairs that are not defined in the TSP in a dictionary + ''' + self.others = {} + if params: + self.others = copy.deepcopy(params) + + def print(self): + print(f'XY title: {self.title}') + + common_x_axis = False + if hasattr(self, 'common_x_axis'): + common_x_axis = self.common_x_axis + print(f'XY has common X axis: {common_x_axis}') + + for series in self.series: + series.print() + for other_item in self.others.items(): + print(f'XY other item: {other_item}') + + +class XYSeries(object): + ''' + Represent a XY series and its values + ''' + + def __init__(self, params): + ''' + Name of the series + ''' + if SERIES_NAME_KEY in params: + self.series_name = params.get(SERIES_NAME_KEY) + del params[SERIES_NAME_KEY] + + ''' + Ìd of the series + ''' + if SERIES_ID_KEY in params: + self.series_id = params.get(SERIES_ID_KEY) + del params[SERIES_ID_KEY] + + ''' + Description of the X axis + ''' + if X_AXIS_KEY in params: + self.x_axis = XYAxis(params.get(X_AXIS_KEY)) + del params[X_AXIS_KEY] + + ''' + Description of the Y axis + ''' + if Y_AXIS_KEY in params: + self.y_axis = XYAxis(params.get(Y_AXIS_KEY)) + del params[Y_AXIS_KEY] + + ''' + Series' X values + ''' + self.x_values = [] + if X_VALUES_KEY in params: + for x_value in params.get(X_VALUES_KEY): + self.x_values.append(x_value) + del params[X_VALUES_KEY] + + ''' + Series' Y values + ''' + self.y_values = [] + if Y_VALUES_KEY in params: + for y_value in params.get(Y_VALUES_KEY): + self.y_values.append(y_value) + del params[Y_VALUES_KEY] + + ''' + Array of tags for each XY value, used when a value passes a filter + ''' + self.tags = [] + if TAGS_KEY in params: + for tag in params.get(TAGS_KEY): + self.tags.append(tag) + del params[TAGS_KEY] + + ''' + Store other key/value pairs that are not defined in the TSP in a dictionary + ''' + self.others = {} + if params: + self.others = copy.deepcopy(params) + + def print(self): + print(f' Series name: {self.series_name}') + print(f' Series id: {self.series_id}') + + if hasattr(self, 'x_axis'): + print(f' Series X-axis:\n{self.x_axis.print()}') + print(f' Series Y-axis:\n{self.y_axis.print()}') + for value in self.x_values: + print(f' Series X-value: {value}') + for value in self.y_values: + print(f' Series Y-value: {value}') + for tag in self.tags: + print(f' Series tag: {tag}') + for other_item in self.others.items(): + print(f' Series other item: {other_item}') + + +class XYAxis(object): + ''' + Description of an axis for XY chart + ''' + + def __init__(self, params): + ''' + Label of the axis + ''' + if LABEL_KEY in params: + self.label = params.get(LABEL_KEY) + del params[LABEL_KEY] + + ''' + The units used for the axis, to be appended to the numbers + ''' + if UNIT_KEY in params: + self.unit = params.get(UNIT_KEY) + del params[UNIT_KEY] + + ''' + Type of data for this axis, to give hint on number formatting + ''' + if DATA_TYPE_KEY in params: + self.data_type = params.get(DATA_TYPE_KEY) + del params[DATA_TYPE_KEY] + + ''' + Store other key/value pairs that are not defined in the TSP in a dictionary + ''' + self.others = {} + if params: + self.others = copy.deepcopy(params) + + def print(self): + print(f' Axis label: {self.label}') + print(f' Axis unit: {self.unit}') + print(f' Axis data type: {self.data_type}') + for other_item in self.others.items(): + print(f' Axis other item: {other_item}')