diff --git a/client/template_web_client/openapi_server/__init__.py b/client/template_web_client/openapi_server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/template_web_client/openapi_server/__main__.py b/client/template_web_client/openapi_server/__main__.py new file mode 100644 index 0000000..aa77bc7 --- /dev/null +++ b/client/template_web_client/openapi_server/__main__.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +import connexion + +from openapi_server import encoder + + +def main(): + app = connexion.App(__name__, specification_dir='./openapi/') + app.app.json_encoder = encoder.JSONEncoder + app.add_api('openapi.yaml', + arguments={'title': 'Prediction Microservice'}, + pythonic_params=True) + + app.run(port=8080) + + +if __name__ == '__main__': + main() diff --git a/client/template_web_client/openapi_server/controllers/__init__.py b/client/template_web_client/openapi_server/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/client/template_web_client/openapi_server/controllers/default_controller.py b/client/template_web_client/openapi_server/controllers/default_controller.py new file mode 100644 index 0000000..3e61229 --- /dev/null +++ b/client/template_web_client/openapi_server/controllers/default_controller.py @@ -0,0 +1,21 @@ +import connexion +from typing import Dict +from typing import Tuple +from typing import Union + +from openapi_server.models.metric import Metric # noqa: E501 +from openapi_server import util +from openapi_server.prediction_service import utils + + +def get_metric_by_id(metric_id): # noqa: E501 + """Retrieve historical and predicted time series by ID + + Returns a time series metric # noqa: E501 + + :param metric_id: Id of the metric to return + :type metric_id: str + + :rtype: Union[Metric, Tuple[Metric, int], Tuple[Metric, int, Dict[str, str]] + """ + return utils.query(metric_id) diff --git a/client/template_web_client/openapi_server/controllers/security_controller.py b/client/template_web_client/openapi_server/controllers/security_controller.py new file mode 100644 index 0000000..6d294ff --- /dev/null +++ b/client/template_web_client/openapi_server/controllers/security_controller.py @@ -0,0 +1,2 @@ +from typing import List + diff --git a/client/template_web_client/openapi_server/encoder.py b/client/template_web_client/openapi_server/encoder.py new file mode 100644 index 0000000..8682003 --- /dev/null +++ b/client/template_web_client/openapi_server/encoder.py @@ -0,0 +1,19 @@ +from json import JSONEncoder + +from openapi_server.models.base_model import Model + + +class JSONEncoder(JSONEncoder): + include_nulls = False + + def default(self, o): + if isinstance(o, Model): + dikt = {} + for attr in o.openapi_types: + value = getattr(o, attr) + if value is None and not self.include_nulls: + continue + attr = o.attribute_map[attr] + dikt[attr] = value + return dikt + return JSONEncoder.default(self, o) diff --git a/client/template_web_client/openapi_server/models/__init__.py b/client/template_web_client/openapi_server/models/__init__.py new file mode 100644 index 0000000..1023dcd --- /dev/null +++ b/client/template_web_client/openapi_server/models/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +# import models into model package +from openapi_server.models.metric import Metric diff --git a/client/template_web_client/openapi_server/models/base_model.py b/client/template_web_client/openapi_server/models/base_model.py new file mode 100644 index 0000000..c01b423 --- /dev/null +++ b/client/template_web_client/openapi_server/models/base_model.py @@ -0,0 +1,68 @@ +import pprint + +import typing + +from openapi_server import util + +T = typing.TypeVar('T') + + +class Model: + # openapiTypes: The key is attribute name and the + # value is attribute type. + openapi_types: typing.Dict[str, type] = {} + + # attributeMap: The key is attribute name and the + # value is json key in definition. + attribute_map: typing.Dict[str, str] = {} + + @classmethod + def from_dict(cls: typing.Type[T], dikt) -> T: + """Returns the dict as a model""" + return util.deserialize_model(dikt, cls) + + def to_dict(self): + """Returns the model properties as a dict + + :rtype: dict + """ + result = {} + + for attr in self.openapi_types: + value = getattr(self, attr) + if isinstance(value, list): + result[attr] = list(map( + lambda x: x.to_dict() if hasattr(x, "to_dict") else x, + value + )) + elif hasattr(value, "to_dict"): + result[attr] = value.to_dict() + elif isinstance(value, dict): + result[attr] = dict(map( + lambda item: (item[0], item[1].to_dict()) + if hasattr(item[1], "to_dict") else item, + value.items() + )) + else: + result[attr] = value + + return result + + def to_str(self): + """Returns the string representation of the model + + :rtype: str + """ + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + """Returns true if both objects are not equal""" + return not self == other diff --git a/client/template_web_client/openapi_server/models/metric.py b/client/template_web_client/openapi_server/models/metric.py new file mode 100644 index 0000000..7efe161 --- /dev/null +++ b/client/template_web_client/openapi_server/models/metric.py @@ -0,0 +1,269 @@ +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from openapi_server.models.base_model import Model +from openapi_server import util + + +class Metric(Model): + """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + + Do not edit the class manually. + """ + + def __init__(self, metric_id=None, timeseries=None, forecasting_values=None, forecasting_upper_bounds=None, forecasting_lower_bounds=None, forecasting_model=None, forecasting_period=None, time=None, aggregation_interval=None): # noqa: E501 + """Metric - a model defined in OpenAPI + + :param metric_id: The metric_id of this Metric. # noqa: E501 + :type metric_id: str + :param timeseries: The timeseries of this Metric. # noqa: E501 + :type timeseries: List[float] + :param forecasting_values: The forecasting_values of this Metric. # noqa: E501 + :type forecasting_values: List[float] + :param forecasting_upper_bounds: The forecasting_upper_bounds of this Metric. # noqa: E501 + :type forecasting_upper_bounds: List[float] + :param forecasting_lower_bounds: The forecasting_lower_bounds of this Metric. # noqa: E501 + :type forecasting_lower_bounds: List[float] + :param forecasting_model: The forecasting_model of this Metric. # noqa: E501 + :type forecasting_model: str + :param forecasting_period: The forecasting_period of this Metric. # noqa: E501 + :type forecasting_period: int + :param time: The time of this Metric. # noqa: E501 + :type time: List[date] + :param aggregation_interval: The aggregation_interval of this Metric. # noqa: E501 + :type aggregation_interval: int + """ + self.openapi_types = { + 'metric_id': str, + 'timeseries': List[float], + 'forecasting_values': List[float], + 'forecasting_upper_bounds': List[float], + 'forecasting_lower_bounds': List[float], + 'forecasting_model': str, + 'forecasting_period': int, + 'time': List[date], + 'aggregation_interval': int + } + + self.attribute_map = { + 'metric_id': 'metricId', + 'timeseries': 'timeseries', + 'forecasting_values': 'forecasting_values', + 'forecasting_upper_bounds': 'forecasting_upper_bounds', + 'forecasting_lower_bounds': 'forecasting_lower_bounds', + 'forecasting_model': 'forecasting_model', + 'forecasting_period': 'forecasting_period', + 'time': 'time', + 'aggregation_interval': 'aggregation_interval' + } + + self._metric_id = metric_id + self._timeseries = timeseries + self._forecasting_values = forecasting_values + self._forecasting_upper_bounds = forecasting_upper_bounds + self._forecasting_lower_bounds = forecasting_lower_bounds + self._forecasting_model = forecasting_model + self._forecasting_period = forecasting_period + self._time = time + self._aggregation_interval = aggregation_interval + + @classmethod + def from_dict(cls, dikt) -> 'Metric': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The Metric of this Metric. # noqa: E501 + :rtype: Metric + """ + return util.deserialize_model(dikt, cls) + + @property + def metric_id(self) -> str: + """Gets the metric_id of this Metric. + + + :return: The metric_id of this Metric. + :rtype: str + """ + return self._metric_id + + @metric_id.setter + def metric_id(self, metric_id: str): + """Sets the metric_id of this Metric. + + + :param metric_id: The metric_id of this Metric. + :type metric_id: str + """ + + self._metric_id = metric_id + + @property + def timeseries(self) -> List[float]: + """Gets the timeseries of this Metric. + + + :return: The timeseries of this Metric. + :rtype: List[float] + """ + return self._timeseries + + @timeseries.setter + def timeseries(self, timeseries: List[float]): + """Sets the timeseries of this Metric. + + + :param timeseries: The timeseries of this Metric. + :type timeseries: List[float] + """ + + self._timeseries = timeseries + + @property + def forecasting_values(self) -> List[float]: + """Gets the forecasting_values of this Metric. + + + :return: The forecasting_values of this Metric. + :rtype: List[float] + """ + return self._forecasting_values + + @forecasting_values.setter + def forecasting_values(self, forecasting_values: List[float]): + """Sets the forecasting_values of this Metric. + + + :param forecasting_values: The forecasting_values of this Metric. + :type forecasting_values: List[float] + """ + + self._forecasting_values = forecasting_values + + @property + def forecasting_upper_bounds(self) -> List[float]: + """Gets the forecasting_upper_bounds of this Metric. + + + :return: The forecasting_upper_bounds of this Metric. + :rtype: List[float] + """ + return self._forecasting_upper_bounds + + @forecasting_upper_bounds.setter + def forecasting_upper_bounds(self, forecasting_upper_bounds: List[float]): + """Sets the forecasting_upper_bounds of this Metric. + + + :param forecasting_upper_bounds: The forecasting_upper_bounds of this Metric. + :type forecasting_upper_bounds: List[float] + """ + + self._forecasting_upper_bounds = forecasting_upper_bounds + + @property + def forecasting_lower_bounds(self) -> List[float]: + """Gets the forecasting_lower_bounds of this Metric. + + + :return: The forecasting_lower_bounds of this Metric. + :rtype: List[float] + """ + return self._forecasting_lower_bounds + + @forecasting_lower_bounds.setter + def forecasting_lower_bounds(self, forecasting_lower_bounds: List[float]): + """Sets the forecasting_lower_bounds of this Metric. + + + :param forecasting_lower_bounds: The forecasting_lower_bounds of this Metric. + :type forecasting_lower_bounds: List[float] + """ + + self._forecasting_lower_bounds = forecasting_lower_bounds + + @property + def forecasting_model(self) -> str: + """Gets the forecasting_model of this Metric. + + + :return: The forecasting_model of this Metric. + :rtype: str + """ + return self._forecasting_model + + @forecasting_model.setter + def forecasting_model(self, forecasting_model: str): + """Sets the forecasting_model of this Metric. + + + :param forecasting_model: The forecasting_model of this Metric. + :type forecasting_model: str + """ + + self._forecasting_model = forecasting_model + + @property + def forecasting_period(self) -> int: + """Gets the forecasting_period of this Metric. + + + :return: The forecasting_period of this Metric. + :rtype: int + """ + return self._forecasting_period + + @forecasting_period.setter + def forecasting_period(self, forecasting_period: int): + """Sets the forecasting_period of this Metric. + + + :param forecasting_period: The forecasting_period of this Metric. + :type forecasting_period: int + """ + + self._forecasting_period = forecasting_period + + @property + def time(self) -> List[date]: + """Gets the time of this Metric. + + + :return: The time of this Metric. + :rtype: List[date] + """ + return self._time + + @time.setter + def time(self, time: List[date]): + """Sets the time of this Metric. + + + :param time: The time of this Metric. + :type time: List[date] + """ + + self._time = time + + @property + def aggregation_interval(self) -> int: + """Gets the aggregation_interval of this Metric. + + + :return: The aggregation_interval of this Metric. + :rtype: int + """ + return self._aggregation_interval + + @aggregation_interval.setter + def aggregation_interval(self, aggregation_interval: int): + """Sets the aggregation_interval of this Metric. + + + :param aggregation_interval: The aggregation_interval of this Metric. + :type aggregation_interval: int + """ + + self._aggregation_interval = aggregation_interval diff --git a/client/template_web_client/openapi_server/openapi/openapi.yaml b/client/template_web_client/openapi_server/openapi/openapi.yaml new file mode 100644 index 0000000..05aaee9 --- /dev/null +++ b/client/template_web_client/openapi_server/openapi/openapi.yaml @@ -0,0 +1,96 @@ +openapi: 3.0.0 +info: + contact: + email: Guangyuan.Piao@dell.com + description: "This is prediction microservice as part of the Novel Metadata Fabric\ + \ based on the OpenAPI 3.1 specification. It provides a forecasting functionality\ + \ for pre-defined metrics such as daily energy consumption metrics on the GLACIATION\ + \ platform. You can retrieve the most recent history and prediction of energy\ + \ consumption of the platform.\n\nYou can find out more about\nthe prediction\ + \ microservice at [https://github.com/glaciation-heu/IceStream/tree/development/prediction_service](https://github.com/glaciation-heu/IceStream/tree/development/prediction_service). " + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + title: Prediction Microservice + version: 0.1.1 +externalDocs: + description: Find out more about prediction microservice + url: https://github.com/glaciation-heu/IceStream/tree/development/prediction_service +servers: +- url: http://0.0.0.0:8080 +paths: + /prediction/{metricId}: + get: + description: Returns a time series metric + operationId: get_metric_by_id + parameters: + - description: Id of the metric to return + explode: false + in: path + name: metricId + required: true + schema: + type: string + style: simple + responses: + "200": + content: + applicatin/json: + schema: + $ref: '#/components/schemas/Metric' + description: successful operation + "400": + description: Invalid Id supplied + "404": + description: Metric not found + summary: Retrieve historical and predicted time series by ID + x-openapi-router-controller: openapi_server.controllers.default_controller +components: + schemas: + Metric: + properties: + metricId: + title: metricId + type: string + timeseries: + items: + format: float + type: number + title: timeseries + type: array + forecasting_values: + items: + format: float + type: number + title: forecasting_values + type: array + forecasting_upper_bounds: + items: + format: float + type: number + title: forecasting_upper_bounds + type: array + forecasting_lower_bounds: + items: + format: float + type: number + title: forecasting_lower_bounds + type: array + forecasting_model: + title: forecasting_model + type: string + forecasting_period: + format: int32 + title: forecasting_period + type: integer + time: + items: + format: date + type: string + title: time + type: array + aggregation_interval: + format: int64 + title: aggregation_interval + type: integer + title: Metric diff --git a/client/template_web_client/openapi_server/prediction_service/__init__.py b/client/template_web_client/openapi_server/prediction_service/__init__.py new file mode 100644 index 0000000..1a4baf5 --- /dev/null +++ b/client/template_web_client/openapi_server/prediction_service/__init__.py @@ -0,0 +1 @@ + diff --git a/client/template_web_client/openapi_server/prediction_service/utils.py b/client/template_web_client/openapi_server/prediction_service/utils.py new file mode 100644 index 0000000..963f49a --- /dev/null +++ b/client/template_web_client/openapi_server/prediction_service/utils.py @@ -0,0 +1,88 @@ +import json + +from openapi_server.models.metric import Metric +from SPARQLWrapper import SPARQLWrapper, JSON + + +def query(metricID): + sparql = SPARQLWrapper("http://192.168.0.94:3030/ds/sparql") + sparql.setReturnFormat(JSON) + + # gets the first 3 geological ages + # from a Geological Timescale database, + # via a SPARQL endpoint + + query = """ + prefix rdf: + prefix om: + prefix prov: + prefix xsd: + + SELECT ?measure ?date + WHERE { + ?subject rdf:type om:Measure . + ?subject om:hasNumericalValue ?measure . + ?subject prov:generatedAtTime ?date . + } + ORDER BY ASC(?date) + LIMIT 30 + """ + + sparql.setQuery(""" + SELECT ?a + WHERE { + ?a ?b . + } + LIMIT 3 + """ + ) + + sparql.setQuery(query) + + try: + # Using SPARQL results + #ret = sparql.queryAndConvert() + #metric = Metric(id=metricID, time=[], value=[]) + #for r in ret["results"]["bindings"]: + # metric.value.append(float(r['measure']['value'])) + # metric.time.append(r['date']['value']) + #print(metric) + #ts_dict = { + # 'id': metricID, + # 'time': metric.time, + # 'value': metric.value + #} + + # Dummpy data for testing + ts_dict = { + "aggregation_interval": 90, + "forecasting_lower_bounds": [ + 4 + ], + "forecasting_model": "ARIMA", + "forecasting_period": 1, + "forecasting_upper_bounds": [ + 6 + ], + "forecasting_values": [ + 5 + ], + "metricId": metricID, + "time": [ + "2024-03-20" + ], + "timeseries": [ + 1,2,3,4 + ] + } + + ts_json = json.dumps(ts_dict) + + return ts_json + except Exception as e: + print(e) + return str(e) + + +if __name__ == '__main__': + print(query(0)) diff --git a/client/template_web_client/openapi_server/test/__init__.py b/client/template_web_client/openapi_server/test/__init__.py new file mode 100644 index 0000000..364aba9 --- /dev/null +++ b/client/template_web_client/openapi_server/test/__init__.py @@ -0,0 +1,16 @@ +import logging + +import connexion +from flask_testing import TestCase + +from openapi_server.encoder import JSONEncoder + + +class BaseTestCase(TestCase): + + def create_app(self): + logging.getLogger('connexion.operation').setLevel('ERROR') + app = connexion.App(__name__, specification_dir='../openapi/') + app.app.json_encoder = JSONEncoder + app.add_api('openapi.yaml', pythonic_params=True) + return app.app diff --git a/client/template_web_client/openapi_server/test/test_default_controller.py b/client/template_web_client/openapi_server/test/test_default_controller.py new file mode 100644 index 0000000..910d380 --- /dev/null +++ b/client/template_web_client/openapi_server/test/test_default_controller.py @@ -0,0 +1,29 @@ +import unittest + +from flask import json + +from openapi_server.models.metric import Metric # noqa: E501 +from openapi_server.test import BaseTestCase + + +class TestDefaultController(BaseTestCase): + """DefaultController integration test stubs""" + + def test_get_metric_by_id(self): + """Test case for get_metric_by_id + + Retrieve historical and predicted time series by ID + """ + headers = { + 'Accept': 'applicatin/json', + } + response = self.client.open( + '/prediction/{metric_id}'.format(metric_id=56), + method='GET', + headers=headers) + self.assert200(response, + 'Response body is : ' + response.data.decode('utf-8')) + + +if __name__ == '__main__': + unittest.main() diff --git a/client/template_web_client/openapi_server/typing_utils.py b/client/template_web_client/openapi_server/typing_utils.py new file mode 100644 index 0000000..74e3c91 --- /dev/null +++ b/client/template_web_client/openapi_server/typing_utils.py @@ -0,0 +1,30 @@ +import sys + +if sys.version_info < (3, 7): + import typing + + def is_generic(klass): + """ Determine whether klass is a generic class """ + return type(klass) == typing.GenericMeta + + def is_dict(klass): + """ Determine whether klass is a Dict """ + return klass.__extra__ == dict + + def is_list(klass): + """ Determine whether klass is a List """ + return klass.__extra__ == list + +else: + + def is_generic(klass): + """ Determine whether klass is a generic class """ + return hasattr(klass, '__origin__') + + def is_dict(klass): + """ Determine whether klass is a Dict """ + return klass.__origin__ == dict + + def is_list(klass): + """ Determine whether klass is a List """ + return klass.__origin__ == list diff --git a/client/template_web_client/openapi_server/util.py b/client/template_web_client/openapi_server/util.py new file mode 100644 index 0000000..bd83c5b --- /dev/null +++ b/client/template_web_client/openapi_server/util.py @@ -0,0 +1,147 @@ +import datetime + +import typing +from openapi_server import typing_utils + + +def _deserialize(data, klass): + """Deserializes dict, list, str into an object. + + :param data: dict, list or str. + :param klass: class literal, or string of class name. + + :return: object. + """ + if data is None: + return None + + if klass in (int, float, str, bool, bytearray): + return _deserialize_primitive(data, klass) + elif klass == object: + return _deserialize_object(data) + elif klass == datetime.date: + return deserialize_date(data) + elif klass == datetime.datetime: + return deserialize_datetime(data) + elif typing_utils.is_generic(klass): + if typing_utils.is_list(klass): + return _deserialize_list(data, klass.__args__[0]) + if typing_utils.is_dict(klass): + return _deserialize_dict(data, klass.__args__[1]) + else: + return deserialize_model(data, klass) + + +def _deserialize_primitive(data, klass): + """Deserializes to primitive type. + + :param data: data to deserialize. + :param klass: class literal. + + :return: int, long, float, str, bool. + :rtype: int | long | float | str | bool + """ + try: + value = klass(data) + except UnicodeEncodeError: + value = data + except TypeError: + value = data + return value + + +def _deserialize_object(value): + """Return an original value. + + :return: object. + """ + return value + + +def deserialize_date(string): + """Deserializes string to date. + + :param string: str. + :type string: str + :return: date. + :rtype: date + """ + if string is None: + return None + + try: + from dateutil.parser import parse + return parse(string).date() + except ImportError: + return string + + +def deserialize_datetime(string): + """Deserializes string to datetime. + + The string should be in iso8601 datetime format. + + :param string: str. + :type string: str + :return: datetime. + :rtype: datetime + """ + if string is None: + return None + + try: + from dateutil.parser import parse + return parse(string) + except ImportError: + return string + + +def deserialize_model(data, klass): + """Deserializes list or dict to model. + + :param data: dict, list. + :type data: dict | list + :param klass: class literal. + :return: model object. + """ + instance = klass() + + if not instance.openapi_types: + return data + + for attr, attr_type in instance.openapi_types.items(): + if data is not None \ + and instance.attribute_map[attr] in data \ + and isinstance(data, (list, dict)): + value = data[instance.attribute_map[attr]] + setattr(instance, attr, _deserialize(value, attr_type)) + + return instance + + +def _deserialize_list(data, boxed_type): + """Deserializes a list and its elements. + + :param data: list to deserialize. + :type data: list + :param boxed_type: class literal. + + :return: deserialized list. + :rtype: list + """ + return [_deserialize(sub_data, boxed_type) + for sub_data in data] + + +def _deserialize_dict(data, boxed_type): + """Deserializes a dict and its elements. + + :param data: dict to deserialize. + :type data: dict + :param boxed_type: class literal. + + :return: deserialized dict. + :rtype: dict + """ + return {k: _deserialize(v, boxed_type) + for k, v in data.items() }