diff --git a/proto/kpi_value.proto b/proto/kpi_value_api.proto similarity index 92% rename from proto/kpi_value.proto rename to proto/kpi_value_api.proto index 4f9f4edc5f0577f6b67f6c584936ec25377603fd..19069f547b5995862b6fcfdcbb997a8f45f6fca3 100644 --- a/proto/kpi_value.proto +++ b/proto/kpi_value_api.proto @@ -4,7 +4,7 @@ package kpi_value_api; import "context.proto"; import "kpi_manager.proto"; -service KpiValueAPI { +service KpiValueAPIService { rpc StoreKpiValues (KpiValueList) returns (context.Empty) {} rpc SelectKpiValues (KpiValueFilter) returns (KpiValueList) {} } diff --git a/src/common/Constants.py b/src/common/Constants.py index 229bd15f148a179ab8262c01ccb27761d73ef74b..c616fed0985e77d2d57b03c8e939e5f6b1a2b52e 100644 --- a/src/common/Constants.py +++ b/src/common/Constants.py @@ -44,6 +44,7 @@ class ServiceNameEnum(Enum): POLICY = 'policy' MONITORING = 'monitoring' KPIMANAGER = 'kpiManager' + KPIVALUEAPI = 'kpiValueApi' TELEMETRYFRONTEND = 'telemetryfrontend' DLT = 'dlt' NBI = 'nbi' @@ -77,8 +78,6 @@ DEFAULT_SERVICE_GRPC_PORTS = { ServiceNameEnum.ZTP .value : 5050, ServiceNameEnum.POLICY .value : 6060, ServiceNameEnum.MONITORING .value : 7070, - ServiceNameEnum.KPIMANAGER .value : 7071, - ServiceNameEnum.TELEMETRYFRONTEND .value : 7072, ServiceNameEnum.DLT .value : 8080, ServiceNameEnum.NBI .value : 9090, ServiceNameEnum.L3_CAD .value : 10001, @@ -94,6 +93,9 @@ DEFAULT_SERVICE_GRPC_PORTS = { ServiceNameEnum.E2EORCHESTRATOR .value : 10050, ServiceNameEnum.OPTICALCONTROLLER .value : 10060, ServiceNameEnum.BGPLS .value : 20030, + ServiceNameEnum.KPIMANAGER .value : 30010, + ServiceNameEnum.KPIVALUEAPI .value : 30020, + ServiceNameEnum.TELEMETRYFRONTEND .value : 30050, # Used for test and debugging only ServiceNameEnum.DLT_GATEWAY .value : 50051, diff --git a/src/common/tools/kafka/Variables.py b/src/common/tools/kafka/Variables.py new file mode 100644 index 0000000000000000000000000000000000000000..afe1ee67be2edbeadb41aeb61d3ff24d3aa73576 --- /dev/null +++ b/src/common/tools/kafka/Variables.py @@ -0,0 +1,27 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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. + +from enum import Enum + +class KafkaConfig(Enum): + SERVER_IP = "127.0.0.1:9092" + +class KafkaTopic(Enum): + REQUEST = 'topic_request' + RESPONSE = 'topic_response' + RAW = 'topic_raw' + LABELED = 'topic_labeled' + VALUE = 'topic_value' + +# create all topics after the deployments (Telemetry and Analytics) \ No newline at end of file diff --git a/src/kpi_manager/service/KpiManagerService.py b/src/kpi_manager/service/KpiManagerService.py index 3868a848f38745b538153629af1c2a327de4ce8b..434246a43d1685ce478a0b330afa635e6999bf75 100755 --- a/src/kpi_manager/service/KpiManagerService.py +++ b/src/kpi_manager/service/KpiManagerService.py @@ -14,10 +14,11 @@ from common.Constants import ServiceNameEnum from common.Settings import get_service_port_grpc -from common.proto.kpi_manager_pb2_grpc import add_KpiManagerServiceServicer_to_server +from monitoring.service.NameMapping import NameMapping from common.tools.service.GenericGrpcService import GenericGrpcService +from common.proto.kpi_manager_pb2_grpc import add_KpiManagerServiceServicer_to_server from kpi_manager.service.KpiManagerServiceServicerImpl import KpiManagerServiceServicerImpl -from monitoring.service.NameMapping import NameMapping + class KpiManagerService(GenericGrpcService): def __init__(self, name_mapping : NameMapping, cls_name: str = __name__) -> None: diff --git a/src/kpi_manager/tests/test_kpi_manager.py b/src/kpi_manager/tests/test_kpi_manager.py index b517c9568fe55e5d4d83f5a1b0542605e86d3500..2f475cc0fdb03ad1e9da2b0614d5a2873d914923 100755 --- a/src/kpi_manager/tests/test_kpi_manager.py +++ b/src/kpi_manager/tests/test_kpi_manager.py @@ -53,7 +53,7 @@ KPIMANAGER_SERVICE_PORT = MOCKSERVICE_PORT + get_service_port_grpc(ServiceNameEn os.environ[get_env_var_name(ServiceNameEnum.KPIMANAGER, ENVVAR_SUFIX_SERVICE_HOST )] = str(LOCAL_HOST) os.environ[get_env_var_name(ServiceNameEnum.KPIMANAGER, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(KPIMANAGER_SERVICE_PORT) -METRICSDB_HOSTNAME = os.environ.get('METRICSDB_HOSTNAME') +# METRICSDB_HOSTNAME = os.environ.get('METRICSDB_HOSTNAME') LOGGER = logging.getLogger(__name__) @@ -230,7 +230,7 @@ def kpi_manager_client(kpi_manager_service : KpiManagerService): # pylint: disab # assert isinstance(response, KpiDescriptorList) def test_set_list_of_KPIs(kpi_manager_client): - LOGGER.info(" >>> test_set_list_of_KPIs: START <<< ") + LOGGER.debug(" >>> test_set_list_of_KPIs: START <<< ") KPIs_TO_SEARCH = ["node_in_power_total", "node_in_current_total", "node_out_power_total"] # adding KPI for kpi in KPIs_TO_SEARCH: diff --git a/src/kpi_value_api/__init__.py b/src/kpi_value_api/__init__.py index 1549d9811aa5d1c193a44ad45d0d7773236c0612..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 100644 --- a/src/kpi_value_api/__init__.py +++ b/src/kpi_value_api/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/src/kpi_value_api/client/KpiValueApiClient.py b/src/kpi_value_api/client/KpiValueApiClient.py new file mode 100644 index 0000000000000000000000000000000000000000..adf17da5d283fec66d3cd24e0fb7b000f877b3e0 --- /dev/null +++ b/src/kpi_value_api/client/KpiValueApiClient.py @@ -0,0 +1,63 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 grpc, logging + +from common.Constants import ServiceNameEnum +from common.Settings import get_service_host, get_service_port_grpc +from common.tools.client.RetryDecorator import retry, delay_exponential +from common.tools.grpc.Tools import grpc_message_to_json_string + +from common.proto.context_pb2 import Empty +from common.proto.kpi_value_api_pb2 import KpiValue, KpiValueList, KpiValueType, KpiValueFilter +from common.proto.kpi_value_api_pb2_grpc import KpiValueAPIServiceStub + +LOGGER = logging.getLogger(__name__) +MAX_RETRIES = 10 +DELAY_FUNCTION = delay_exponential(initial=0.01, increment=2.0, maximum=5.0) +RETRY_DECORATOR = retry(max_retries=MAX_RETRIES, delay_function=DELAY_FUNCTION, prepare_method_name='connect') + +class KpiValueApiClient: + def __init__(self, host=None, port=None): + if not host: host = get_service_host(ServiceNameEnum.KPIVALUEAPI) + if not port: port = get_service_port_grpc(ServiceNameEnum.KPIVALUEAPI) + self.endpoint = '{:s}:{:s}'.format(str(host), str(port)) + LOGGER.debug('Creating channel to {:s}...'.format(str(self.endpoint))) + self.channel = None + self.stub = None + self.connect() + LOGGER.debug('Channel created') + + def connect(self): + self.channel = grpc.insecure_channel(self.endpoint) + self.stub = KpiValueAPIServiceStub(self.channel) + + def close(self): + if self.channel is not None: self.channel.close() + self.channel = None + self.stub = None + + @RETRY_DECORATOR + def StoreKpiValues(self, request: KpiValueList) -> Empty: + LOGGER.debug('StoreKpiValues: {:s}'.format(grpc_message_to_json_string(request))) + response = self.stub.StoreKpiValues(request) + LOGGER.debug('StoreKpiValues result: {:s}'.format(grpc_message_to_json_string(response))) + return response + + @RETRY_DECORATOR + def SelectKpiValues(self, request: KpiValueFilter) -> KpiValueList: + LOGGER.debug('SelectKpiValues: {:s}'.format(grpc_message_to_json_string(request))) + response = self.stub.SelectKpiValues(request) + LOGGER.debug('SelectKpiValues result: {:s}'.format(grpc_message_to_json_string(response))) + return response \ No newline at end of file diff --git a/src/kpi_value_api/client/__init__.py b/src/kpi_value_api/client/__init__.py index 1549d9811aa5d1c193a44ad45d0d7773236c0612..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 100644 --- a/src/kpi_value_api/client/__init__.py +++ b/src/kpi_value_api/client/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/src/kpi_value_api/service/KpiValueApiService.py b/src/kpi_value_api/service/KpiValueApiService.py new file mode 100644 index 0000000000000000000000000000000000000000..2fb24aaacff82ce7c33cce6e43e2f50f4af14fbe --- /dev/null +++ b/src/kpi_value_api/service/KpiValueApiService.py @@ -0,0 +1,31 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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. + + +from .NameMapping import NameMapping +from common.Constants import ServiceNameEnum +from common.Settings import get_service_port_grpc +from common.tools.service.GenericGrpcService import GenericGrpcService +from .KpiValueApiServiceServicerImpl import KpiValueApiServiceServicerImpl +from common.proto.kpi_value_api_pb2_grpc import add_KpiValueAPIServiceServicer_to_server + + +class KpiValueApiService(GenericGrpcService): + def __init__(self, name_mapping : NameMapping, cls_name : str = __name__ ) -> None: + port = get_service_port_grpc(ServiceNameEnum.KPIVALUEAPI) + super().__init__(port, cls_name=cls_name) + self.kpiValueApiService_servicer = KpiValueApiServiceServicerImpl(name_mapping) + + def install_servicers(self): + add_KpiValueAPIServiceServicer_to_server(self.kpiValueApiService_servicer, self.server) \ No newline at end of file diff --git a/src/kpi_value_api/service/KpiValueApiServiceServicerImpl.py b/src/kpi_value_api/service/KpiValueApiServiceServicerImpl.py new file mode 100644 index 0000000000000000000000000000000000000000..e0b8b550a6f6ad0a7d1018e69705eee26180d241 --- /dev/null +++ b/src/kpi_value_api/service/KpiValueApiServiceServicerImpl.py @@ -0,0 +1,65 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 logging, grpc +from typing import Tuple, Any +from common.method_wrappers.Decorator import MetricsPool, safe_and_metered_rpc_method +from common.tools.kafka.Variables import KafkaConfig, KafkaTopic + +from common.proto.context_pb2 import Empty +from common.proto.kpi_value_api_pb2_grpc import KpiValueAPIServiceServicer +from common.proto.kpi_value_api_pb2 import KpiValueList, KpiValueFilter + +from confluent_kafka import Producer as KafkaProducer + +from .NameMapping import NameMapping + + +LOGGER = logging.getLogger(__name__) +METRICS_POOL = MetricsPool('KpiValueAPI', 'NBIgRPC') + +class KpiValueApiServiceServicerImpl(KpiValueAPIServiceServicer): + def __init__(self, name_mapping : NameMapping): + LOGGER.debug('Init KpiValueApiService') + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def StoreKpiValues(self, request: KpiValueList, grpc_context: grpc.ServicerContext + ) -> Empty: + LOGGER.debug('StoreKpiValues: Received gRPC message object: {:}'.format(request)) + producer_obj = KafkaProducer({'bootstrap.servers' : KafkaConfig.SERVER_IP.value}) + for kpi_value in request.kpi_value_list: + kpi_value_to_produce : Tuple [str, Any, Any] = ( + kpi_value.kpi_id.kpi_id, # kpi_value.kpi_id.kpi_id.uuid + kpi_value.timestamp, # kpi_value.timestamp.timestamp + kpi_value.kpi_value_type # kpi_value.kpi_value_type.(many options) + ) + LOGGER.debug('KPI to produce is {:}'.format(kpi_value_to_produce)) + msg_key = "gRPC-KpiValueApi" # str(__class__.__name__) + # write this KPI to Kafka + producer_obj.produce(KafkaTopic.VALUE.value, + key = msg_key, + value = str(kpi_value_to_produce), + callback = self.delivery_callback + ) + producer_obj.flush() + return Empty() + + @safe_and_metered_rpc_method(METRICS_POOL, LOGGER) + def SelectKpiValues(self, request: KpiValueFilter, grpc_context: grpc.ServicerContext + ) -> KpiValueList: + LOGGER.debug('SelectKpiValues: Received gRPC message object: {:}'.format(request)) + + def delivery_callback(self, err, msg): + if err: print(f'Message delivery failed: {err}') + else: print(f'Message delivered to topic {msg.topic()}') diff --git a/src/kpi_value_api/service/NameMapping.py b/src/kpi_value_api/service/NameMapping.py new file mode 100644 index 0000000000000000000000000000000000000000..f98e367b17b4a2e4c7c6f3dcdb90dfb8ee24d3ad --- /dev/null +++ b/src/kpi_value_api/service/NameMapping.py @@ -0,0 +1,46 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 threading +from typing import Dict, Optional + +class NameMapping: + def __init__(self) -> None: + self.__lock = threading.Lock() + self.__device_to_name : Dict[str, str] = dict() + self.__endpoint_to_name : Dict[str, str] = dict() + + def get_device_name(self, device_uuid : str) -> Optional[str]: + with self.__lock: + return self.__device_to_name.get(device_uuid) + + def get_endpoint_name(self, endpoint_uuid : str) -> Optional[str]: + with self.__lock: + return self.__endpoint_to_name.get(endpoint_uuid) + + def set_device_name(self, device_uuid : str, device_name : str) -> None: + with self.__lock: + self.__device_to_name[device_uuid] = device_name + + def set_endpoint_name(self, endpoint_uuid : str, endpoint_name : str) -> None: + with self.__lock: + self.__endpoint_to_name[endpoint_uuid] = endpoint_name + + def delete_device_name(self, device_uuid : str) -> None: + with self.__lock: + self.__device_to_name.pop(device_uuid, None) + + def delete_endpoint_name(self, endpoint_uuid : str) -> None: + with self.__lock: + self.__endpoint_to_name.pop(endpoint_uuid, None) diff --git a/src/kpi_value_api/service/__init__.py b/src/kpi_value_api/service/__init__.py index 1549d9811aa5d1c193a44ad45d0d7773236c0612..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 100644 --- a/src/kpi_value_api/service/__init__.py +++ b/src/kpi_value_api/service/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/src/kpi_value_api/tests/messages.py b/src/kpi_value_api/tests/messages.py new file mode 100644 index 0000000000000000000000000000000000000000..b06f4ab1d415ce64bd52e11e29a6a53760a19325 --- /dev/null +++ b/src/kpi_value_api/tests/messages.py @@ -0,0 +1,35 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 uuid, time +from common.proto.kpi_value_api_pb2 import KpiValue, KpiValueList + + +def create_kpi_value_list(): + _create_kpi_value_list = KpiValueList() + # To run this experiment sucessfully, already existing UUID in KPI DB in necessary. + # because the UUID is used to get the descriptor form KPI DB. + EXISTING_KPI_IDs = ["198a5a83-ddd3-4818-bdcb-e468eda03e18", + "c288ea27-db40-419e-81d3-f675df22c8f4", + str(uuid.uuid4())] + + for kpi_id_uuid in EXISTING_KPI_IDs: + kpi_value_object = KpiValue() + kpi_value_object.kpi_id.kpi_id.uuid = kpi_id_uuid + kpi_value_object.timestamp.timestamp = float(time.time()) + kpi_value_object.kpi_value_type.floatVal = 100 + + _create_kpi_value_list.kpi_value_list.append(kpi_value_object) + + return _create_kpi_value_list \ No newline at end of file diff --git a/src/kpi_value_api/tests/test_kpi_value_api.py b/src/kpi_value_api/tests/test_kpi_value_api.py new file mode 100644 index 0000000000000000000000000000000000000000..bfa9485a8aa322866f4ec46a5b8ed82758ee900e --- /dev/null +++ b/src/kpi_value_api/tests/test_kpi_value_api.py @@ -0,0 +1,83 @@ +# Copyright 2022-2024 ETSI OSG/SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/) +# +# 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 os, logging, pytest + +from common.proto.context_pb2 import Empty +from common.Constants import ServiceNameEnum +from common.Settings import ( + ENVVAR_SUFIX_SERVICE_HOST, ENVVAR_SUFIX_SERVICE_PORT_GRPC, get_env_var_name, get_service_port_grpc) + +from kpi_value_api.service.NameMapping import NameMapping +from kpi_value_api.service.KpiValueApiService import KpiValueApiService +from kpi_value_api.client.KpiValueApiClient import KpiValueApiClient +from kpi_value_api.tests.messages import create_kpi_value_list + +LOCAL_HOST = '127.0.0.1' + +KPIVALUEAPI_SERVICE_PORT = get_service_port_grpc(ServiceNameEnum.KPIVALUEAPI) # type: ignore +os.environ[get_env_var_name(ServiceNameEnum.KPIVALUEAPI, ENVVAR_SUFIX_SERVICE_HOST )] = str(LOCAL_HOST) +os.environ[get_env_var_name(ServiceNameEnum.KPIVALUEAPI, ENVVAR_SUFIX_SERVICE_PORT_GRPC)] = str(KPIVALUEAPI_SERVICE_PORT) + +LOGGER = logging.getLogger(__name__) + +# This fixture will be requested by test cases and last during testing session +@pytest.fixture(scope='session') +def kpi_value_api_service(): + LOGGER.info('Initializing KpiValueApiService...') + name_mapping = NameMapping() + # _service = MonitoringService(name_mapping) + _service = KpiValueApiService(name_mapping) + _service.start() + + # yield the server, when test finishes, execution will resume to stop it + LOGGER.info('Yielding KpiValueApiService...') + yield _service + + LOGGER.info('Terminating KpiValueApiService...') + _service.stop() + + LOGGER.info('Terminated KpiValueApiService...') + +# This fixture will be requested by test cases and last during testing session. +# The client requires the server, so client fixture has the server as dependency. +@pytest.fixture(scope='session') +def kpi_value_api_client(kpi_value_api_service : KpiValueApiService ): + LOGGER.info('Initializing KpiValueApiClient...') + _client = KpiValueApiClient() + + # yield the server, when test finishes, execution will resume to stop it + LOGGER.info('Yielding KpiValueApiClient...') + yield _client + + LOGGER.info('Closing KpiValueApiClient...') + _client.close() + + LOGGER.info('Closed KpiValueApiClient...') + +################################################## +# Prepare Environment, should be the first test +################################################## + +# To be added here + +########################### +# Tests Implementation of Kpi Value Api +########################### + +def test_store_kpi_values(kpi_value_api_client): + LOGGER.debug(" >>> test_set_list_of_KPIs: START <<< ") + response = kpi_value_api_client.StoreKpiValues(create_kpi_value_list()) + assert isinstance(response, Empty)