
Python 2.y and Python <= 3.2 are no longer supported, so we no longer need to maintain logics for such old versions. Also the check_python_version method is removed by this change, because the required python version is now enforced by setup.cfg. Change-Id: Ifc5e618a791bdf591e0ec61075089dd992c73e0c
543 lines
17 KiB
Python
543 lines
17 KiB
Python
# Copyright 2015 Tesora Inc.
|
|
# 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.
|
|
|
|
import abc
|
|
import ast
|
|
import configparser
|
|
import csv
|
|
import io
|
|
import re
|
|
|
|
import xmltodict
|
|
import yaml
|
|
|
|
from oslo_serialization import base64
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import strutils
|
|
|
|
from trove.common import utils as trove_utils
|
|
|
|
|
|
class StringConverter(object):
|
|
"""A passthrough string-to-object converter.
|
|
"""
|
|
|
|
def __init__(self, object_mappings):
|
|
"""
|
|
:param object_mappings: string-to-object mappings
|
|
:type object_mappings: dict
|
|
"""
|
|
self._object_mappings = object_mappings
|
|
|
|
def to_strings(self, items):
|
|
"""Recursively convert collection items to strings.
|
|
|
|
:returns: Copy of the input collection with all items converted.
|
|
"""
|
|
if trove_utils.is_collection(items):
|
|
return map(self.to_strings, items)
|
|
|
|
return self._to_string(items)
|
|
|
|
def to_objects(self, items):
|
|
"""Recursively convert collection string to objects.
|
|
|
|
:returns: Copy of the input collection with all items converted.
|
|
"""
|
|
if trove_utils.is_collection(items):
|
|
return map(self.to_objects, items)
|
|
|
|
return self._to_object(items)
|
|
|
|
def _to_string(self, value):
|
|
for k, v in self._object_mappings.items():
|
|
if v is value:
|
|
return k
|
|
|
|
return str(value)
|
|
|
|
def _to_object(self, value):
|
|
# Return known mappings and quoted strings right away.
|
|
if value in self._object_mappings:
|
|
return self._object_mappings[value]
|
|
elif (isinstance(value, str) and
|
|
re.match("^'(.*)'|\"(.*)\"$", value)):
|
|
return value
|
|
|
|
try:
|
|
return ast.literal_eval(value)
|
|
except Exception:
|
|
return value
|
|
|
|
|
|
class StreamCodec(object, metaclass=abc.ABCMeta):
|
|
|
|
@abc.abstractmethod
|
|
def serialize(self, data):
|
|
"""Serialize a Python object into a stream.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def deserialize(self, stream):
|
|
"""Deserialize stream data into a Python structure.
|
|
"""
|
|
|
|
|
|
class IdentityCodec(StreamCodec):
|
|
"""
|
|
A basic passthrough codec.
|
|
Does not modify the data in any way.
|
|
"""
|
|
|
|
def serialize(self, data):
|
|
return data
|
|
|
|
def deserialize(self, stream):
|
|
return stream
|
|
|
|
|
|
class YamlCodec(StreamCodec):
|
|
"""
|
|
Read/write data from/into a YAML config file.
|
|
|
|
a: 1
|
|
b: {c: 3, d: 4}
|
|
...
|
|
|
|
The above file content (flow-style) would be represented as:
|
|
{'a': 1,
|
|
'b': {'c': 3, 'd': 4,}
|
|
...
|
|
}
|
|
"""
|
|
|
|
def __init__(self, default_flow_style=False):
|
|
"""
|
|
:param default_flow_style: Use flow-style (inline) formatting of
|
|
nested collections.
|
|
:type default_flow_style: boolean
|
|
"""
|
|
self._default_flow_style = default_flow_style
|
|
|
|
def serialize(self, dict_data):
|
|
return yaml.dump(dict_data, Dumper=self.dumper,
|
|
default_flow_style=self._default_flow_style)
|
|
|
|
def deserialize(self, stream):
|
|
return yaml.load(stream, Loader=self.loader)
|
|
|
|
@property
|
|
def loader(self):
|
|
return yaml.loader.Loader
|
|
|
|
@property
|
|
def dumper(self):
|
|
return yaml.dumper.Dumper
|
|
|
|
|
|
class SafeYamlCodec(YamlCodec):
|
|
"""
|
|
Same as YamlCodec except that it uses safe Loader and Dumper which
|
|
encode Unicode strings and produce only basic YAML tags.
|
|
"""
|
|
|
|
def __init__(self, default_flow_style=False):
|
|
super(SafeYamlCodec, self).__init__(
|
|
default_flow_style=default_flow_style)
|
|
|
|
@property
|
|
def loader(self):
|
|
return yaml.loader.SafeLoader
|
|
|
|
@property
|
|
def dumper(self):
|
|
return yaml.dumper.SafeDumper
|
|
|
|
|
|
class IniCodec(StreamCodec):
|
|
"""
|
|
Read/write data from/into an ini-style config file.
|
|
|
|
[section_1]
|
|
key = value
|
|
key = value
|
|
...
|
|
|
|
[section_2]
|
|
key = value
|
|
key = value
|
|
...
|
|
|
|
The above file content would be represented as:
|
|
{'section_1': {'key': value, 'key': value, ...},
|
|
'section_2': {'key': value, 'key': value, ...}
|
|
...
|
|
}
|
|
"""
|
|
|
|
def __init__(self, default_value=None, comment_markers=('#', ';')):
|
|
"""
|
|
:param default_value: Default value for keys with no value.
|
|
If set, all keys are written as 'key = value'.
|
|
The key is written without trailing '=' if None.
|
|
:type default_value: object
|
|
"""
|
|
self._default_value = default_value
|
|
self._comment_markers = comment_markers
|
|
|
|
def serialize(self, dict_data):
|
|
parser = self._init_config_parser(dict_data)
|
|
output = io.StringIO()
|
|
parser.write(output)
|
|
|
|
return output.getvalue()
|
|
|
|
def deserialize(self, stream):
|
|
parser = self._init_config_parser()
|
|
parser.read_file(self._pre_parse(stream))
|
|
|
|
return {s: {k:
|
|
StringConverter({None: self._default_value}).to_objects(v)
|
|
for k, v in parser.items(s, raw=True)}
|
|
for s in parser.sections()}
|
|
|
|
def _pre_parse(self, stream):
|
|
buf = io.StringIO()
|
|
for line in io.StringIO(stream):
|
|
# Ignore commented lines.
|
|
if not line.startswith(self._comment_markers):
|
|
# Strip leading and trailing whitespaces from each line.
|
|
buf.write(line.strip() + '\n')
|
|
|
|
# Rewind the output buffer.
|
|
buf.flush()
|
|
buf.seek(0)
|
|
|
|
return buf
|
|
|
|
def _init_config_parser(self, sections=None):
|
|
parser = configparser.ConfigParser(allow_no_value=True)
|
|
if sections:
|
|
for section in sections:
|
|
parser.add_section(section)
|
|
for key, value in sections[section].items():
|
|
str_val = StringConverter(
|
|
{self._default_value: None}).to_strings(value)
|
|
parser.set(section, key,
|
|
str(str_val) if str_val is not None
|
|
else str_val)
|
|
|
|
return parser
|
|
|
|
|
|
class PropertiesCodec(StreamCodec):
|
|
"""
|
|
Read/write data from/into a property-style config file.
|
|
|
|
key1 k1arg1 k1arg2 ... k1argN
|
|
key2 k2arg1 k2arg2 ... k2argN
|
|
key3 k3arg1 k3arg2 ...
|
|
key3 k3arg3 k3arg4 ...
|
|
...
|
|
|
|
The above file content would be represented as:
|
|
{'key1': [k1arg1, k1arg2 ... k1argN],
|
|
'key2': [k2arg1, k2arg2 ... k2argN]
|
|
'key3': [[k3arg1, k3arg2, ...], [k3arg3, k3arg4, ...]]
|
|
...
|
|
}
|
|
"""
|
|
|
|
QUOTING_MODE = csv.QUOTE_MINIMAL
|
|
STRICT_MODE = False
|
|
SKIP_INIT_SPACE = True
|
|
|
|
def __init__(self, delimiter=' ', comment_markers=('#'),
|
|
unpack_singletons=True, string_mappings=None):
|
|
"""
|
|
:param delimiter: A one-character used to separate fields.
|
|
:type delimiter: string
|
|
|
|
:param empty_value: Value to represent None in the output.
|
|
:type empty_value: object
|
|
|
|
:param comment_markers: List of comment markers.
|
|
:type comment_markers: list
|
|
|
|
:param unpack_singletons: Whether to unpack singleton collections
|
|
(collections with only a single item).
|
|
:type unpack_singletons: boolean
|
|
|
|
:param string_mappings: User-defined string representations of
|
|
Python objects.
|
|
:type string_mappings: dict
|
|
"""
|
|
self._delimiter = delimiter
|
|
self._comment_markers = comment_markers
|
|
self._string_converter = StringConverter(string_mappings or {})
|
|
self._unpack_singletons = unpack_singletons
|
|
|
|
def serialize(self, dict_data):
|
|
output = io.StringIO()
|
|
writer = csv.writer(output, delimiter=self._delimiter,
|
|
quoting=self.QUOTING_MODE,
|
|
strict=self.STRICT_MODE,
|
|
skipinitialspace=self.SKIP_INIT_SPACE)
|
|
|
|
for key, value in dict_data.items():
|
|
writer.writerows(self._to_rows(key, value))
|
|
|
|
return output.getvalue()
|
|
|
|
def deserialize(self, stream):
|
|
reader = csv.reader(io.StringIO(stream),
|
|
delimiter=self._delimiter,
|
|
quoting=self.QUOTING_MODE,
|
|
strict=self.STRICT_MODE,
|
|
skipinitialspace=self.SKIP_INIT_SPACE)
|
|
|
|
return self._to_dict(reader)
|
|
|
|
def _to_dict(self, reader):
|
|
data_dict = {}
|
|
for row in reader:
|
|
if row:
|
|
key = row[0].strip()
|
|
# Ignore comment lines.
|
|
if not key.strip().startswith(self._comment_markers):
|
|
# NOTE(zhaochao): a list object is expected for
|
|
# trove_utils.unpack_singleton, however in python3
|
|
# map objects won't be treated as lists, so we
|
|
# convert the result of StringConverter.to_objects
|
|
# to a list explicitly.
|
|
items = list(self._string_converter.to_objects(
|
|
[v if v else None for v in
|
|
map(self._strip_comments, row[1:])]))
|
|
current = data_dict.get(key)
|
|
if current is not None:
|
|
current.append(trove_utils.unpack_singleton(items)
|
|
if self._unpack_singletons else items)
|
|
else:
|
|
data_dict.update({key: [items]})
|
|
|
|
if self._unpack_singletons:
|
|
# Unpack singleton values.
|
|
# NOTE(zhaochao): In Python 3.x, dict.items() returns a view
|
|
# object, which will reflect the changes of the dict itself:
|
|
# https://docs.python.org/3/library/stdtypes.html#dict-views
|
|
# This means as we're changing the dict, dict.items() cannot
|
|
# guarantee we're safely iterating all entries in the dict.
|
|
# Manually converting the result of dict.items() to a list will
|
|
# fix.
|
|
for k, v in list(data_dict.items()):
|
|
data_dict.update({k: trove_utils.unpack_singleton(v)})
|
|
|
|
return data_dict
|
|
|
|
def _strip_comments(self, value):
|
|
# Strip in-line comments.
|
|
for marker in self._comment_markers:
|
|
value = value.split(marker)[0]
|
|
return value.strip()
|
|
|
|
def _to_rows(self, header, items):
|
|
rows = []
|
|
if trove_utils.is_collection(items):
|
|
if any(trove_utils.is_collection(item) for item in items):
|
|
# This is multi-row property.
|
|
for item in items:
|
|
rows.extend(self._to_rows(header, item))
|
|
else:
|
|
# This is a single-row property with multiple arguments.
|
|
rows.append(self._to_list(
|
|
header, self._string_converter.to_strings(items)))
|
|
else:
|
|
# This is a single-row property with only one argument.
|
|
# Note(zhaochao): csv.writerows expects a list object before
|
|
# python 3.5, but map objects won't be treated as lists in
|
|
# python 3, so we explicitly convert the result of
|
|
# StringConverter.to_strings to a list here to support py34
|
|
# unittests.
|
|
rows.append(
|
|
list(self._string_converter.to_strings(
|
|
self._to_list(header, items))))
|
|
|
|
return rows
|
|
|
|
def _to_list(self, *items):
|
|
container = []
|
|
for item in items:
|
|
if trove_utils.is_collection(item):
|
|
# This item is a nested collection - unpack it.
|
|
container.extend(self._to_list(*item))
|
|
else:
|
|
# This item is not a collection - append it to the list.
|
|
container.append(item)
|
|
|
|
return container
|
|
|
|
|
|
class KeyValueCodec(StreamCodec):
|
|
"""
|
|
Read/write data from/into a simple key=value file.
|
|
|
|
key1=value1
|
|
key2=value2
|
|
key3=value3
|
|
...
|
|
|
|
The above file content would be represented as:
|
|
{'key1': 'value1',
|
|
'key2': 'value2',
|
|
'key3': 'value3',
|
|
...
|
|
}
|
|
"""
|
|
|
|
BOOL_PYTHON = 0 # True, False
|
|
BOOL_LOWER = 1 # true, false
|
|
BOOL_UPPER = 2 # TRUE, FALSE
|
|
|
|
def __init__(self, delimiter='=',
|
|
comment_marker='#',
|
|
line_terminator='\r\n',
|
|
value_quoting=False,
|
|
value_quote_char="'",
|
|
bool_case=BOOL_PYTHON,
|
|
big_ints=False,
|
|
hidden_marker=None):
|
|
"""
|
|
:param delimiter: string placed between key and value
|
|
:param comment_marker: string indicating comment line in file
|
|
:param line_terminator: string placed between lines
|
|
:param value_quoting: whether or not to quote string values
|
|
:param value_quote_char: character used to quote string values
|
|
:param bool_case: BOOL_* setting case of bool values
|
|
:param big_ints: treat K/M/G at the end of ints as an int
|
|
:param hidden_marker: pattern prefixing hidden param
|
|
"""
|
|
self._delimeter = delimiter
|
|
self._comment_marker = comment_marker
|
|
self._line_terminator = line_terminator
|
|
self._value_quoting = value_quoting
|
|
self._value_quote_char = value_quote_char
|
|
self._bool_case = bool_case
|
|
self._big_ints = big_ints
|
|
self._hidden_marker = hidden_marker
|
|
|
|
def serialize(self, dict_data):
|
|
lines = []
|
|
for k, v in dict_data.items():
|
|
lines.append(k + self._delimeter + self.serialize_value(v))
|
|
return self._line_terminator.join(lines)
|
|
|
|
def deserialize(self, stream):
|
|
# Note(zhaochao): In Python 3, when files are opened in text mode,
|
|
# newlines will be translated to '\n' by default, so we just split
|
|
# the stream by '\n'.
|
|
lines = stream.split('\n')
|
|
|
|
result = {}
|
|
for line in lines:
|
|
line = line.lstrip().rstrip()
|
|
if line == '' or line.startswith(self._comment_marker):
|
|
continue
|
|
k, v = re.split(re.escape(self._delimeter), line, 1)
|
|
if self._value_quoting and v.startswith(self._value_quote_char):
|
|
# remove trailing comments
|
|
v = re.sub(r'%s *%s.*$' % ("'", '#'), '', v)
|
|
v = v.lstrip(
|
|
self._value_quote_char).rstrip(
|
|
self._value_quote_char)
|
|
elif v.lower() in ['true', 'false']:
|
|
v = strutils.bool_from_string(v.lower())
|
|
else:
|
|
# remove trailing comments
|
|
v = re.sub('%s.*$' % self._comment_marker, '', v)
|
|
if self._hidden_marker and v.startswith(self._hidden_marker):
|
|
continue
|
|
result[k.strip()] = v
|
|
return result
|
|
|
|
def serialize_value(self, value):
|
|
if isinstance(value, bool):
|
|
if self._bool_case == self.BOOL_PYTHON:
|
|
value = str(value)
|
|
elif self._bool_case == self.BOOL_LOWER:
|
|
value = str(value).lower()
|
|
elif self._bool_case == self.BOOL_UPPER:
|
|
value = str(value).upper()
|
|
if self.should_quote_value(value):
|
|
value = self._value_quote_char + value + self._value_quote_char
|
|
return str(value)
|
|
|
|
def should_quote_value(self, value):
|
|
if not self._value_quoting:
|
|
return False
|
|
if isinstance(value, bool) or isinstance(value, int):
|
|
return False
|
|
if value.lower() in ['true', 'false']:
|
|
return False
|
|
try:
|
|
int(value)
|
|
return False
|
|
except ValueError:
|
|
pass
|
|
if self._big_ints and re.match(r'\d+[kKmMgGtTpP]', value):
|
|
return False
|
|
return True
|
|
|
|
|
|
class JsonCodec(StreamCodec):
|
|
|
|
def serialize(self, dict_data):
|
|
return jsonutils.dumps(dict_data)
|
|
|
|
def deserialize(self, stream):
|
|
if type(stream) == str:
|
|
return jsonutils.load(io.StringIO(stream))
|
|
if type(stream) == bytes:
|
|
return jsonutils.load(io.BytesIO(stream))
|
|
|
|
|
|
class Base64Codec(StreamCodec):
|
|
"""Serialize (encode) and deserialize (decode) using the base64 codec.
|
|
To read binary data from a file and b64encode it, used the decode=False
|
|
flag on operating_system's read calls. Use encode=False to decode
|
|
binary data before writing to a file as well.
|
|
"""
|
|
|
|
# NOTE(zhaochao): migrate to oslo_serialization.base64 to serialize(return
|
|
# a text object) and deserialize(return a bytes object) data.
|
|
|
|
def serialize(self, data):
|
|
return base64.encode_as_text(data)
|
|
|
|
def deserialize(self, stream):
|
|
return base64.decode_as_bytes(stream)
|
|
|
|
|
|
class XmlCodec(StreamCodec):
|
|
|
|
def __init__(self, encoding='utf-8'):
|
|
self._encoding = encoding
|
|
|
|
def serialize(self, dict_data):
|
|
return xmltodict.unparse(
|
|
dict_data, output=None, encoding=self._encoding, pretty=True)
|
|
|
|
def deserialize(self, stream):
|
|
return xmltodict.parse(stream, encoding=self._encoding)
|