X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=RO%2Fosm_ro%2Futils.py;fp=RO%2Fosm_ro%2Futils.py;h=ac291c1da149236b693f422e2b72af325dd25bdd;hb=7d782eff123e5b44d41437377ccca66ad1e8b21b;hp=0000000000000000000000000000000000000000;hpb=5db670b68349fd1f00a5efc8c0ccd0ef9d073dca;p=osm%2FRO.git diff --git a/RO/osm_ro/utils.py b/RO/osm_ro/utils.py new file mode 100644 index 00000000..ac291c1d --- /dev/null +++ b/RO/osm_ro/utils.py @@ -0,0 +1,429 @@ +# -*- coding: utf-8 -*- + +## +# Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U. +# This file is part of openmano +# All 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. +# +# For those usages not covered by the Apache License, Version 2.0 please +# contact with: nfvlabs@tid.es +## + +''' +utils is a module that implements functions that are used by all openmano modules, +dealing with aspects such as reading/writing files, formatting inputs/outputs for quick translation +from dictionaries to appropriate database dictionaries, etc. +''' +__author__="Alfonso Tierno, Gerardo Garcia" +__date__ ="$08-sep-2014 12:21:22$" + +import datetime +import time +import warnings +from functools import reduce, partial, wraps +from itertools import tee + +from itertools import filterfalse + +from jsonschema import exceptions as js_e +from jsonschema import validate as js_v + +from inspect import getfullargspec as getspec + +#from bs4 import BeautifulSoup + +def read_file(file_to_read): + """Reads a file specified by 'file_to_read' and returns (True,) in case of success or (False, ) in case of failure""" + try: + f = open(file_to_read, 'r') + read_data = f.read() + f.close() + except Exception as e: + return (False, str(e)) + + return (True, read_data) + +def write_file(file_to_write, text): + """Write a file specified by 'file_to_write' and returns (True,NOne) in case of success or (False, ) in case of failure""" + try: + f = open(file_to_write, 'w') + f.write(text) + f.close() + except Exception as e: + return (False, str(e)) + + return (True, None) + +def format_in(http_response, schema): + try: + client_data = http_response.json() + js_v(client_data, schema) + #print "Input data: ", str(client_data) + return True, client_data + except js_e.ValidationError as exc: + print("validate_in error, jsonschema exception ", exc.message, "at", exc.path) + return False, ("validate_in error, jsonschema exception ", exc.message, "at", exc.path) + +def remove_extra_items(data, schema): + deleted = [] + if isinstance(data, (tuple, list)): + for d in data: + a = remove_extra_items(d, schema['items']) + if a: + deleted.append(a) + elif isinstance(data, dict): + # TODO deal with patternProperties + if 'properties' not in schema: + return None + to_delete = [] + for k in data.keys(): + if k in schema['properties']: + a = remove_extra_items(data[k], schema['properties'][k]) + if a: + deleted.append({k: a}) + elif not schema.get('additionalProperties'): + to_delete.append(k) + deleted.append(k) + for k in to_delete: + del data[k] + if len(deleted) == 0: + return None + elif len(deleted) == 1: + return deleted[0] + + return deleted + +#def format_html2text(http_content): +# soup=BeautifulSoup(http_content) +# text = soup.p.get_text() + " " + soup.pre.get_text() +# return text + + +def delete_nulls(var): + if isinstance(var, dict): + to_delete = [] + for k in var.keys(): + if var[k] is None: + to_delete.append([k]) + elif isinstance(var[k], (dict, list, tuple)): + if delete_nulls(var[k]): + to_delete.append(k) + for k in to_delete: + del var[k] + if len(var) == 0: + return True + elif isinstance(var, (list, tuple)): + for k in var: + if isinstance(k, dict): + delete_nulls(k) + if len(var) == 0: + return True + return False + + +def convert_bandwidth(data, reverse=False): + '''Check the field bandwidth recursivelly and when found, it removes units and convert to number + It assumes that bandwidth is well formed + Attributes: + 'data': dictionary bottle.FormsDict variable to be checked. None or empty is consideted valid + 'reverse': by default convert form str to int (Mbps), if True it convert from number to units + Return: + None + ''' + if isinstance(data, dict): + for k in data.keys(): + if isinstance(data[k], (dict, tuple, list)): + convert_bandwidth(data[k], reverse) + if "bandwidth" in data: + try: + value=str(data["bandwidth"]) + if not reverse: + pos = value.find("bps") + if pos>0: + if value[pos-1]=="G": + data["bandwidth"] = int(data["bandwidth"][:pos-1]) * 1000 + elif value[pos-1]=="k": + data["bandwidth"]= int(data["bandwidth"][:pos-1]) // 1000 + else: + data["bandwidth"]= int(data["bandwidth"][:pos]) + else: + value = int(data["bandwidth"]) + if value % 1000 == 0 and value > 1000: + data["bandwidth"] = str(value // 1000) + " Gbps" + else: + data["bandwidth"] = str(value) + " Mbps" + except: + print("convert_bandwidth exception for type", type(data["bandwidth"]), " data", data["bandwidth"]) + return + if isinstance(data, (tuple, list)): + for k in data: + if isinstance(k, (dict, tuple, list)): + convert_bandwidth(k, reverse) + +def convert_float_timestamp2str(var): + '''Converts timestamps (created_at, modified_at fields) represented as float + to a string with the format '%Y-%m-%dT%H:%i:%s' + It enters recursively in the dict var finding this kind of variables + ''' + if type(var) is dict: + for k,v in var.items(): + if type(v) is float and k in ("created_at", "modified_at"): + var[k] = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(v) ) + elif type(v) is dict or type(v) is list or type(v) is tuple: + convert_float_timestamp2str(v) + if len(var) == 0: return True + elif type(var) is list or type(var) is tuple: + for v in var: + convert_float_timestamp2str(v) + +def convert_datetime2str(var): + '''Converts a datetime variable to a string with the format '%Y-%m-%dT%H:%i:%s' + It enters recursively in the dict var finding this kind of variables + ''' + if type(var) is dict: + for k,v in var.items(): + if type(v) is datetime.datetime: + var[k]= v.strftime('%Y-%m-%dT%H:%M:%S') + elif type(v) is dict or type(v) is list or type(v) is tuple: + convert_datetime2str(v) + if len(var) == 0: return True + elif type(var) is list or type(var) is tuple: + for v in var: + convert_datetime2str(v) + +def convert_str2boolean(data, items): + '''Check recursively the content of data, and if there is an key contained in items, convert value from string to boolean + Done recursively + Attributes: + 'data': dictionary variable to be checked. None or empty is considered valid + 'items': tuple of keys to convert + Return: + None + ''' + if type(data) is dict: + for k in data.keys(): + if type(data[k]) is dict or type(data[k]) is tuple or type(data[k]) is list: + convert_str2boolean(data[k], items) + if k in items: + if type(data[k]) is str: + if data[k]=="false" or data[k]=="False": data[k]=False + elif data[k]=="true" or data[k]=="True": data[k]=True + if type(data) is tuple or type(data) is list: + for k in data: + if type(k) is dict or type(k) is tuple or type(k) is list: + convert_str2boolean(k, items) + +def check_valid_uuid(uuid): + id_schema = {"type" : "string", "pattern": "^[a-fA-F0-9]{8}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$"} + id_schema2 = {"type" : "string", "pattern": "^[a-fA-F0-9]{32}$"} + try: + js_v(uuid, id_schema) + return True + except js_e.ValidationError: + try: + js_v(uuid, id_schema2) + return True + except js_e.ValidationError: + return False + return False + + +def expand_brackets(text): + """ + Change a text with TEXT[ABC..] into a list with [TEXTA, TEXTB, TEXC, ... + if no bracket is used it just return the a list with the single text + It uses recursivity to allow several [] in the text + :param text: + :return: + """ + if text is None: + return (None, ) + start = text.find("[") + end = text.find("]") + if start < 0 or end < 0: + return [text] + text_list = [] + for char in text[start+1:end]: + text_list += expand_brackets(text[:start] + char + text[end+1:]) + return text_list + +def deprecated(message): + def deprecated_decorator(func): + def deprecated_func(*args, **kwargs): + warnings.warn("{} is a deprecated function. {}".format(func.__name__, message), + category=DeprecationWarning, + stacklevel=2) + warnings.simplefilter('default', DeprecationWarning) + return func(*args, **kwargs) + return deprecated_func + return deprecated_decorator + + +def truncate(text, max_length=1024): + """Limit huge texts in number of characters""" + text = str(text) + if text and len(text) >= max_length: + return text[:max_length//2-3] + " ... " + text[-max_length//2+3:] + return text + + +def merge_dicts(*dicts, **kwargs): + """Creates a new dict merging N others and keyword arguments. + Right-most dicts take precedence. + Keyword args take precedence. + """ + return reduce( + lambda acc, x: acc.update(x) or acc, + list(dicts) + [kwargs], {}) + + +def remove_none_items(adict): + """Return a similar dict without keys associated to None values""" + return {k: v for k, v in adict.items() if v is not None} + + +def filter_dict_keys(adict, allow): + """Return a similar dict, but just containing the explicitly allowed keys + + Arguments: + adict (dict): Simple python dict data struct + allow (list): Explicits allowed keys + """ + return {k: v for k, v in adict.items() if k in allow} + + +def filter_out_dict_keys(adict, deny): + """Return a similar dict, but not containing the explicitly denied keys + + Arguments: + adict (dict): Simple python dict data struct + deny (list): Explicits denied keys + """ + return {k: v for k, v in adict.items() if k not in deny} + + +def expand_joined_fields(record): + """Given a db query result, explode the fields that contains `.` (join + operations). + + Example + >> expand_joined_fiels({'wim.id': 2}) + # {'wim': {'id': 2}} + """ + result = {} + for field, value in record.items(): + keys = field.split('.') + target = result + target = reduce(lambda target, key: target.setdefault(key, {}), + keys[:-1], result) + target[keys[-1]] = value + + return result + + +def ensure(condition, exception): + """Raise an exception if condition is not met""" + if not condition: + raise exception + + +def partition(predicate, iterable): + """Create two derived iterators from a single one + The first iterator created will loop thought the values where the function + predicate is True, the second one will iterate over the values where it is + false. + """ + iterable1, iterable2 = tee(iterable) + return filter(predicate, iterable2), filterfalse(predicate, iterable1) + + +def pipe(*functions): + """Compose functions of one argument in the opposite order, + So pipe(f, g)(x) = g(f(x)) + """ + return lambda x: reduce(lambda acc, f: f(acc), functions, x) + + +def compose(*functions): + """Compose functions of one argument, + So compose(f, g)(x) = f(g(x)) + """ + return lambda x: reduce(lambda acc, f: f(acc), functions[::-1], x) + + +def safe_get(target, key_path, default=None): + """Given a path of keys (eg.: "key1.key2.key3"), return a nested value in + a nested dict if present, or the default value + """ + keys = key_path.split('.') + target = reduce(lambda acc, key: acc.get(key) or {}, keys[:-1], target) + return target.get(keys[-1], default) + + +class Attempt(object): + """Auxiliary class to be used in an attempt to retry executing a failing + procedure + + Attributes: + count (int): 0-based "retries" counter + max_attempts (int): maximum number of "retries" allowed + info (dict): extra information about the specific attempt + (can be used to produce more meaningful error messages) + """ + __slots__ = ('count', 'max', 'info') + + MAX = 3 + + def __init__(self, count=0, max_attempts=MAX, info=None): + self.count = count + self.max = max_attempts + self.info = info or {} + + @property + def countdown(self): + """Like count, but in the opposite direction""" + return self.max - self.count + + @property + def number(self): + """1-based counter""" + return self.count + 1 + + +def inject_args(fn=None, **args): + """Partially apply keyword arguments in a function, but only if the function + define them in the first place + """ + if fn is None: # Allows calling the decorator directly or with parameters + return partial(inject_args, **args) + + spec = getspec(fn) + return wraps(fn)(partial(fn, **filter_dict_keys(args, spec.args))) + + +def get_arg(name, fn, args, kwargs): + """Find the value of an argument for a function, given its argument list. + + This function can be used to display more meaningful errors for debugging + """ + if name in kwargs: + return kwargs[name] + + spec = getspec(fn) + if name in spec.args: + i = spec.args.index(name) + return args[i] if i < len(args) else None + + return None