Initial commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
'''
|
||||
A library for parsing configuration files in various formats.
|
||||
|
||||
Modules:
|
||||
- _configuration: Base classes.
|
||||
- ini: INI configuration.
|
||||
- json: JSON configuration.
|
||||
- exceptions: Exceptions for configuration parsing.
|
||||
- parse: A package including the parsers.
|
||||
- serialize: A package including the serializers.
|
||||
'''
|
||||
|
||||
from ._configuration import *
|
||||
from ._configuration import __all__ as _configuration__all__
|
||||
from . import exceptions
|
||||
from . import json
|
||||
from . import ini
|
||||
from . import parse
|
||||
from . import serialize
|
||||
|
||||
__all__ = [
|
||||
'exceptions',
|
||||
'json',
|
||||
'ini',
|
||||
'parse',
|
||||
'serialize',
|
||||
*_configuration__all__
|
||||
]
|
||||
@@ -0,0 +1,129 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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 copy
|
||||
from types import FunctionType
|
||||
from typing import Any
|
||||
|
||||
__all__ = [
|
||||
'Configuration',
|
||||
'set_mutability',
|
||||
'is_mutable',
|
||||
]
|
||||
|
||||
class Configuration:
|
||||
def __init__(self, _config: dict = {}, mutable: bool = True) -> None:
|
||||
'''A base class for configurations.'''
|
||||
|
||||
self._config = copy.deepcopy(_config)
|
||||
self._mutable = mutable
|
||||
|
||||
def _check_mutability(self) -> None:
|
||||
return self._mutable
|
||||
|
||||
def _mutate(self, func: FunctionType) -> FunctionType:
|
||||
def wrapper(*args, **kwargs) -> Any:
|
||||
if self._check_mutability():
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
raise TypeError('configuration is immutable')
|
||||
return wrapper
|
||||
|
||||
|
||||
def _set(self, name: str, value: str) -> None:
|
||||
@self._mutate
|
||||
def _set(name: str, value: str) -> None:
|
||||
self._config[name] = value
|
||||
|
||||
_set(name, value)
|
||||
|
||||
def _delete(self, name: str) -> None:
|
||||
@self._mutate
|
||||
def _delete(name: str) -> None:
|
||||
del self._config[name]
|
||||
|
||||
_delete(name)
|
||||
|
||||
def _get(self, name: str) -> str:
|
||||
return self._config[name]
|
||||
|
||||
def __getattribute__(self, name: str):
|
||||
if name.startswith('_') or name in self.__dir__():
|
||||
return super().__getattribute__(name)
|
||||
else:
|
||||
if name not in self._config:
|
||||
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
|
||||
return self._get(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name.startswith('_') or name in self.__dir__():
|
||||
super().__setattr__(name, value)
|
||||
else:
|
||||
self._set(name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
if name.startswith('_') or name in self.__dir__():
|
||||
super().__delattr__(name)
|
||||
else:
|
||||
self._delete(name)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._set(key, value)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._config[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
self._delete(key)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._config.items())
|
||||
|
||||
def __contains__(self, item):
|
||||
return item in self._config
|
||||
|
||||
def set_mutability(configuration: Configuration, mutable: bool, *, operate_on_original_object: bool = True) -> Configuration:
|
||||
'''
|
||||
Sets the mutability of a configuration.
|
||||
|
||||
:param configuration: The configuration object.
|
||||
:type configuration: Configuration
|
||||
:param mutable: The new mutability.
|
||||
:type mutable: bool
|
||||
:param operate_on_original_object: If ``True``, the mutability of the
|
||||
original object will be set. If ``False``, the original object will be
|
||||
unchanged. The new configuration will always be returned.
|
||||
:type operate_on_original_object: bool
|
||||
|
||||
:return: The configuration object with the new mutability
|
||||
:rtype: Configuration
|
||||
'''
|
||||
|
||||
if not operate_on_original_object:
|
||||
configuration = copy.deepcopy(configuration)
|
||||
|
||||
configuration._mutable = mutable
|
||||
return configuration
|
||||
|
||||
def is_mutable(configuration: Configuration) -> Configuration:
|
||||
'''
|
||||
Checks whether a configuration is mutable.
|
||||
|
||||
:param configuration: The configuration.
|
||||
:type configuration: Configuration
|
||||
|
||||
:return: The mutability
|
||||
:rtype: bool
|
||||
'''
|
||||
return configuration._mutable
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
__all__ = [
|
||||
'ConfigurationSyntaxError',
|
||||
'EscapeSequenceSyntaxError',
|
||||
'INISyntaxError',
|
||||
'INIInvalidGroupHeader',
|
||||
'INIInvalidKeyValueLine',
|
||||
'JSONSyntaxError',
|
||||
'JSONValueSyntaxError',
|
||||
'JSONNumberSyntaxError',
|
||||
'JSONStringSyntaxError',
|
||||
'JSONBooleanSyntaxError',
|
||||
'JSONNullSyntaxError',
|
||||
'JSONArraySyntaxError',
|
||||
'JSONObjectSyntaxError',
|
||||
'ConfigurationTypeError',
|
||||
'JSONTypeError'
|
||||
]
|
||||
|
||||
class ConfigurationSyntaxError(Exception): ...
|
||||
|
||||
class EscapeSequenceSyntaxError(ConfigurationSyntaxError): ...
|
||||
|
||||
class INISyntaxError(ConfigurationSyntaxError): ...
|
||||
|
||||
class INIInvalidGroupHeader(INISyntaxError): ...
|
||||
class INIInvalidKeyValueLine(INISyntaxError): ...
|
||||
|
||||
|
||||
class JSONSyntaxError(ConfigurationSyntaxError): ...
|
||||
|
||||
class JSONValueSyntaxError(JSONSyntaxError): ...
|
||||
|
||||
class JSONNumberSyntaxError(JSONSyntaxError): ...
|
||||
class JSONStringSyntaxError(JSONSyntaxError): ...
|
||||
class JSONBooleanSyntaxError(JSONSyntaxError): ...
|
||||
class JSONNullSyntaxError(JSONSyntaxError): ...
|
||||
class JSONArraySyntaxError(JSONSyntaxError): ...
|
||||
class JSONObjectSyntaxError(JSONSyntaxError): ...
|
||||
|
||||
|
||||
class ConfigurationTypeError(TypeError): ...
|
||||
|
||||
class JSONTypeError(ConfigurationTypeError): ...
|
||||
@@ -0,0 +1,111 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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 __future__ import annotations
|
||||
import typing
|
||||
from ._configuration import Configuration
|
||||
from .parse.ini import _COMMENT_PREFIXES, _QUOTATION_MARKS, parse_ini
|
||||
from .serialize.ini import serialize as serialize_ini
|
||||
|
||||
__all__ = [
|
||||
'INIConfiguration',
|
||||
'INIConfigurationSection'
|
||||
]
|
||||
|
||||
class INIConfiguration(Configuration):
|
||||
def __iter__(self):
|
||||
return iter({k: dict(v) if isinstance(v, INIConfigurationSection) else v for k, v in self._config.items()}.items())
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, data: str | bytes, comment_prefixes: typing.Collection[str] = _COMMENT_PREFIXES, quotation_marks: typing.Collection[str] = _QUOTATION_MARKS, ignore_errors: bool = False, default: INIConfiguration = None):
|
||||
'''
|
||||
Parses INI configuration from a string and returns an instance of INIConfiguration.
|
||||
|
||||
:param cls: The class method is called on.
|
||||
:type cls: typing.Type[INIConfiguration]
|
||||
:param data: The INI configuration data to parse.
|
||||
:type data: str | bytes
|
||||
:param comment_prefixes: A collection of prefixes that denote comments in the INI file.
|
||||
:type comment_prefixes: typing.Collection[str]
|
||||
:param quotation_marks: A collection of characters used for quoting values in the INI file.
|
||||
:type quotation_marks: typing.Collection[str]
|
||||
:param ignore_errors: If True, parsing errors will be ignored.
|
||||
:type ignore_errors: bool
|
||||
:param default: The default configuration.
|
||||
:type default: INIConfiguration
|
||||
|
||||
:raises INISyntaxError: If there is a syntax error in the INI configuration and ``ignore_errors`` is ``False``.
|
||||
|
||||
:return: An instance of INIConfiguration representing the parsed INI configuration.
|
||||
:rtype: INIConfiguration
|
||||
'''
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode()
|
||||
|
||||
configuration = cls()
|
||||
for section_name, content in parse_ini(data, comment_prefixes=comment_prefixes, quotation_marks=quotation_marks, ignore_errors=ignore_errors).items():
|
||||
group = INIConfigurationSection(section_name)
|
||||
for key, value in content.items():
|
||||
group[key] = value
|
||||
configuration[section_name] = group
|
||||
|
||||
if default is not None:
|
||||
for section in default._config:
|
||||
if section not in configuration:
|
||||
configuration[section] = INIConfigurationSection(section)
|
||||
for propk, propv in default[section]._config.items():
|
||||
if propk not in dict(configuration[section]).keys():
|
||||
configuration[section][propk] = propv
|
||||
|
||||
return configuration
|
||||
|
||||
def to_string(self, separator: str = '='):
|
||||
'''
|
||||
Serializes the INIConfiguration instance to an INI formatted string.
|
||||
|
||||
:param separator: The separator to use between keys and values in the output string.
|
||||
:type separator: str
|
||||
|
||||
:return: An INI formatted string representing the INIConfiguration instance.
|
||||
:rtype: str
|
||||
'''
|
||||
|
||||
return serialize_ini(self._config, separator)
|
||||
|
||||
class INIConfigurationSection(INIConfiguration):
|
||||
def __init__(self, name: str):
|
||||
'''
|
||||
A class representing a group in an INI configuration.
|
||||
|
||||
:param name: The name of the group.
|
||||
:type name: str
|
||||
'''
|
||||
super().__init__()
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def to_string(self, separator: str = '='):
|
||||
'''
|
||||
Serializes the INIConfigurationGroup instance to an INI formatted string.
|
||||
|
||||
:param separator: The separator to use between keys and values in the output string.
|
||||
:type separator: str
|
||||
|
||||
:return: An INI formatted string representing the INIConfigurationGroup instance.
|
||||
:rtype: str
|
||||
'''
|
||||
return ('[' + self._name + ']\n' + super().to_string(separator=separator)).strip()
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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 ._configuration import Configuration
|
||||
from .parse.json import parse_type, JSONObject, JSONString, JSONNumber, JSONNull, JSONBoolean, JSONArray
|
||||
from .exceptions import JSONTypeError
|
||||
|
||||
__all__ = [
|
||||
'JSONConfiguration'
|
||||
]
|
||||
|
||||
def _configuration(data):
|
||||
if isinstance(data, str):
|
||||
return JSONString(data)
|
||||
if isinstance(data, (int, float)):
|
||||
return JSONNumber(data)
|
||||
if data is None:
|
||||
return JSONNull(None)
|
||||
if isinstance(data, bool):
|
||||
return JSONBoolean(data)
|
||||
if isinstance(data, list):
|
||||
return JSONArray(data)
|
||||
if isinstance(data, dict):
|
||||
return JSONObject(data)
|
||||
|
||||
class JSONConfiguration(Configuration):
|
||||
def __iter__(self) -> iter:
|
||||
return iter({k: v.value for k, v in self._config.items()}.items())
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, data: str | bytes, ignore_errors: bool = False):
|
||||
'''
|
||||
Parses JSON configuration from a string and returns an instance of JSONConfiguration.
|
||||
|
||||
:param data: The JSON data
|
||||
:type data: str
|
||||
:param ignore_errors: If True, errors will be ignored.
|
||||
:type ignore_errors: bool
|
||||
|
||||
:raises JSONValueSyntaxError: If a value is invalid and ``ignore_errors`` is ``False``.
|
||||
:raises JSONObjectSyntaxError: If an object is invalid and ``ignore_errors`` is ``False``.
|
||||
:raises JSONArraySyntaxError: If an array is invalid and ``ignore_errors`` is ``False``.
|
||||
:raises JSONStringSyntaxError: If a string is invalid and ``ignore_errors`` is ``False``.
|
||||
:raises JSONNullSyntaxError: If a null value is invalid and ``ignore_errors`` is ``False``.
|
||||
:raises JSONBooleanSyntaxError: If a null value is invalid and ``ignore_errors`` is ``False``.
|
||||
:raises JSONNumberSyntaxError: If a number is invalid and ``ignore_errors`` is ``False``.
|
||||
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid and ``ignore_errors`` is ``False``.
|
||||
|
||||
:return: An instance of JSONConfiguration representing the parsed JSON configuration
|
||||
:rtype: JSONConfiguration
|
||||
'''
|
||||
try:
|
||||
data = parse_type(data).parse(data)
|
||||
if isinstance(data, JSONObject):
|
||||
return cls({k: _configuration(v) for k, v in data.value.items()})
|
||||
elif not ignore_errors:
|
||||
raise JSONTypeError(f'expected object, got {data._type}. Use jcloud_config_parser.parse.json.parse_json to parse JSONs.')
|
||||
except:
|
||||
if ignore_errors:
|
||||
return cls()
|
||||
else:
|
||||
raise
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
'''
|
||||
The parsers
|
||||
|
||||
Modules:
|
||||
- ini: INI parser.
|
||||
- json: JSON parser.
|
||||
'''
|
||||
|
||||
from . import json
|
||||
from . import ini
|
||||
|
||||
__all__ = [
|
||||
'json',
|
||||
'ini'
|
||||
]
|
||||
@@ -0,0 +1,173 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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 typing
|
||||
from ..exceptions import INIInvalidGroupHeader, INIInvalidKeyValueLine, INISyntaxError
|
||||
|
||||
__all__ = [
|
||||
'parse_ini'
|
||||
]
|
||||
|
||||
_QUOTATION_MARKS = ['"', "'"]
|
||||
_COMMENT_PREFIXES = ['#', ';']
|
||||
_KEY_VALUE_LINE = 0
|
||||
_GROUP_HEADER_LINE = 1
|
||||
_COMMENT_LINE = 2
|
||||
|
||||
def _reverse_dict(d: typing.Dict) -> typing.Dict:
|
||||
'''
|
||||
Reverses the order of items in a dictionary.
|
||||
|
||||
:param d: The dictionary to reverse.
|
||||
:type d: typing.Dict
|
||||
|
||||
:return: A new dictionary with the items in reversed order.
|
||||
:rtype: typing.Dict
|
||||
'''
|
||||
return {k: v for k, v in list(reversed(d.items()))}
|
||||
|
||||
def _parse_key_value_line(line: str, comment_prefixes: typing.Collection[str] = _COMMENT_PREFIXES, quotation_marks: typing.Collection[str] = _QUOTATION_MARKS, ignore_errors: bool = False) -> typing.Dict[str, str]:
|
||||
'''
|
||||
Parses a key-value line.
|
||||
|
||||
:param line: The key-value-line
|
||||
:type line: str
|
||||
:param comment_prefixes: A collection of prefixes that denote comments in the INI file.
|
||||
:type comment_prefixes: typing.Collection[str]
|
||||
:param quotation_marks: A collection of characters used for quoting values in the INI file.
|
||||
:type quotation_marks: typing.Collection[str]
|
||||
:param ignore_errors: If True, errors are ignored.
|
||||
:type ignore_errors: bool
|
||||
|
||||
:raises INIInvalidKeyValueLine: If the key-value line is invalid and ignore_errors is False.
|
||||
|
||||
:return: A dictionary with the key and the value (`{'key': 'value'}`) or an empty dictionary if an error has occured and ignore_errors is True.
|
||||
:rtype: Dict[str, str]
|
||||
'''
|
||||
separator = None
|
||||
if ':' in line and '=' not in line:
|
||||
separator = ':'
|
||||
if '=' in line and ':' not in line:
|
||||
separator = '='
|
||||
if ':' in line and '=' in line:
|
||||
separator = '=' if line.index('=') < line.index(':') else ':'
|
||||
if separator is None:
|
||||
if ignore_errors:
|
||||
return {}
|
||||
raise INIInvalidKeyValueLine('the key-value line is invalid')
|
||||
key = line.split(separator)[0].strip()
|
||||
value = separator.join(line.split(separator)[1:]).strip()
|
||||
|
||||
if any(value.endswith(mark) and not value.startswith(mark) for mark in quotation_marks):
|
||||
if ignore_errors:
|
||||
return {}
|
||||
raise INISyntaxError('unterminated literal')
|
||||
|
||||
if any(value.startswith(mark) for mark in quotation_marks):
|
||||
if value[0] == value[-1]:
|
||||
value = value[1:-1]
|
||||
else:
|
||||
if ignore_errors:
|
||||
return {}
|
||||
raise INISyntaxError('unterminated literal')
|
||||
else:
|
||||
for prefix in comment_prefixes:
|
||||
if prefix in value:
|
||||
value = value.split(prefix)[0].strip()
|
||||
|
||||
return {key: value}
|
||||
|
||||
def _parse_group_header(line: str, ignore_errors: bool = False) -> str:
|
||||
'''
|
||||
Parses a group header line.
|
||||
|
||||
:param line: The group header line to parse.
|
||||
:type line: str
|
||||
:param ignore_errors: If True, errors are ignored.
|
||||
:type ignore_errors: bool
|
||||
|
||||
:raises INIInvalidGroupHeader: If the group header is invalid and ignore_errors is False.
|
||||
|
||||
:return: The name of the group.
|
||||
:rtype: str
|
||||
'''
|
||||
if line.startswith('[') and line.endswith(']') and len(line) > 2:
|
||||
return line[1:-1]
|
||||
else:
|
||||
if ignore_errors:
|
||||
return ''
|
||||
raise INIInvalidGroupHeader('the header is invalid')
|
||||
|
||||
def _parse_line_type(line: str, comment_prefixes: typing.Collection[str] = _COMMENT_PREFIXES, ignore_errors: bool = False) -> int:
|
||||
'''
|
||||
Returns the type of a line.
|
||||
|
||||
:param line: The line to check.
|
||||
:type line: str
|
||||
:param comment_prefixes: A collection of prefixes that denote comments in the INI file.
|
||||
:type comment_prefixes: typing.Collection[str]
|
||||
:param ignore_errors: If True, errors are ignored.
|
||||
:type ignore_errors: bool
|
||||
|
||||
:raises INISyntaxError: If the line is invalid and ignore_errors is False.
|
||||
|
||||
:return: The type of the line (COMMENT_LINE, GROUP_HEADER_LINE, or KEY_VALUE_LINE).
|
||||
:rtype: int
|
||||
'''
|
||||
line = line.strip()
|
||||
if any(line.startswith(prefix) for prefix in comment_prefixes) or not line.strip():
|
||||
return _COMMENT_LINE
|
||||
elif line.startswith('[') and line.endswith(']') and len(line) > 2:
|
||||
return _GROUP_HEADER_LINE
|
||||
elif '=' in line or ':' in line:
|
||||
return _KEY_VALUE_LINE
|
||||
else:
|
||||
if ignore_errors:
|
||||
return _COMMENT_LINE
|
||||
raise INISyntaxError('the line is invalid')
|
||||
|
||||
def _compress_conf(conf):
|
||||
while '\n\n' in conf:
|
||||
conf = conf.replace('\n\n', '\n')
|
||||
return conf.strip()
|
||||
|
||||
def parse_ini(conf, comment_prefixes=_COMMENT_PREFIXES, quotation_marks=_QUOTATION_MARKS, ignore_errors: bool = False, global_group: bool = True) -> typing.Dict[str, typing.Dict[str, str]]:
|
||||
'''Parses INI configuration from a string and returns a nested dictionary.
|
||||
|
||||
:param conf: The INI configuration string to parse.
|
||||
:param comment_prefixes: A collection of prefixes that denote comments in the INI file.
|
||||
:param quotation_marks: A collection of characters used for quoting values in the INI file.
|
||||
:param ignore_errors: If True, parsing errors will be ignored.
|
||||
:param global_group: If True, key-value pairs outside of any group will be included. Otherwise, an exception will be raised.
|
||||
|
||||
:return: A nested dictionary representing the parsed INI configuration.
|
||||
|
||||
:raises INISyntaxError: If there is a syntax error in the INI configuration and ignore_errors is False.
|
||||
|
||||
:rtype: typing.Dict[str, typing.Dict[str, str]]
|
||||
'''
|
||||
|
||||
conf = _compress_conf(conf)
|
||||
result = {}
|
||||
current_group = {}
|
||||
for line in list(reversed(conf.split('\n'))):
|
||||
line = line.strip()
|
||||
if _parse_line_type(line, comment_prefixes=comment_prefixes, ignore_errors=ignore_errors) == _COMMENT_LINE:
|
||||
continue
|
||||
if _parse_line_type(line, comment_prefixes=comment_prefixes, ignore_errors=ignore_errors) == _GROUP_HEADER_LINE:
|
||||
result[_parse_group_header(line, ignore_errors=ignore_errors)] = _reverse_dict(current_group)
|
||||
current_group = {}
|
||||
if _parse_line_type(line, comment_prefixes=comment_prefixes, ignore_errors=ignore_errors) == _KEY_VALUE_LINE:
|
||||
current_group = current_group | _parse_key_value_line(line, comment_prefixes=comment_prefixes, quotation_marks=quotation_marks, ignore_errors=ignore_errors)
|
||||
return _reverse_dict(result)
|
||||
@@ -0,0 +1,616 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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 ..exceptions import EscapeSequenceSyntaxError, JSONValueSyntaxError, JSONNumberSyntaxError, JSONStringSyntaxError, JSONBooleanSyntaxError, JSONNullSyntaxError, JSONArraySyntaxError, JSONObjectSyntaxError
|
||||
import typing
|
||||
import re
|
||||
|
||||
__all__ = [
|
||||
'JSONType',
|
||||
'JSONNumber',
|
||||
'JSONString',
|
||||
'JSONBoolean',
|
||||
'JSONNull',
|
||||
'JSONArray',
|
||||
'JSONObject',
|
||||
'parse_escape_sequences',
|
||||
'parse_type',
|
||||
'parse_json'
|
||||
]
|
||||
|
||||
_JSON_ESCAPE_SEQUENCES = {
|
||||
'"': '"',
|
||||
'\\': '\\',
|
||||
'/': '/',
|
||||
'b': '\b',
|
||||
'f': '\f',
|
||||
'n': '\n',
|
||||
'r': '\r',
|
||||
't': '\t',
|
||||
}
|
||||
_JSON_NUMBER_RE = re.compile(
|
||||
r"""
|
||||
-? # optional minus
|
||||
(?:0|[1-9][0-9]*) # integer part
|
||||
(?:\.[0-9]+)? # optional fraction
|
||||
(?:[eE][+-]?[0-9]+)? # optional exponent
|
||||
\Z
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
class JSONType:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return self.value.__repr__()
|
||||
|
||||
class JSONNumber(JSONType):
|
||||
@property
|
||||
def _type(self):
|
||||
return 'number'
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str) -> typing.Union[int, float]:
|
||||
'''
|
||||
Parses a JSON number value.
|
||||
|
||||
:param value: The value
|
||||
:type value: str
|
||||
|
||||
:raises JSONNumberSyntaxError: If the number is not a valid JSON number.
|
||||
:raises JSONTypeError: If the value is not a number.
|
||||
|
||||
:return: The number in ``value``
|
||||
:rtype: JSONNumber
|
||||
'''
|
||||
|
||||
value = value.strip()
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise TypeError('JSON numbers must be provided as strings')
|
||||
|
||||
if not _JSON_NUMBER_RE.match(value):
|
||||
raise JSONNumberSyntaxError(f'Invalid JSON number: {value!r}')
|
||||
|
||||
if '.' not in value and 'e' not in value and 'E' not in value:
|
||||
return cls(int(value))
|
||||
|
||||
return cls(float(value))
|
||||
|
||||
class JSONString(JSONType):
|
||||
@property
|
||||
def _type(self):
|
||||
return 'string'
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str):
|
||||
'''
|
||||
Parses a JSON string value.
|
||||
|
||||
:param value: The value
|
||||
:type value: str
|
||||
|
||||
:raises JSONStringSyntaxError: If the string is not a valid JSON string.
|
||||
|
||||
:return: The string in ``value``
|
||||
:rtype: JSONString
|
||||
'''
|
||||
|
||||
value = value.strip()
|
||||
|
||||
if not (value[0] == '"' and value[-1] == '"' and value.count('"') - value.count('\\"') == 2):
|
||||
raise JSONStringSyntaxError(f'Invalid JSON string (JSON string have to start and end with quotation marks): {value}')
|
||||
else:
|
||||
return cls(parse_escape_sequences(value[1:-1]))
|
||||
|
||||
class JSONBoolean(JSONType):
|
||||
@property
|
||||
def _type(self):
|
||||
return 'boolean'
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str):
|
||||
'''
|
||||
Parses a JSON boolean value.
|
||||
|
||||
:param value: The value
|
||||
:type value: str
|
||||
|
||||
:raises JSONBooleanSyntaxError: If the boolean is neither ``true`` nor ``false``.
|
||||
|
||||
:return: The boolean in ``value``
|
||||
:rtype: JSONBoolean
|
||||
'''
|
||||
|
||||
value = value.strip()
|
||||
|
||||
if value in ('true', 'false'):
|
||||
return cls(True if value == 'true' else False)
|
||||
else:
|
||||
raise JSONBooleanSyntaxError(f'Invalid JSON boolean: {value}')
|
||||
|
||||
class JSONNull(JSONType):
|
||||
@property
|
||||
def _type(self):
|
||||
return 'null'
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str):
|
||||
'''
|
||||
Parses a JSON null value.
|
||||
|
||||
:param value: The value
|
||||
:type value: str
|
||||
|
||||
:raises JSONNullSyntaxError: If the null value is not ``null``.
|
||||
|
||||
:return: The null value
|
||||
:rtype: JSONNull
|
||||
'''
|
||||
|
||||
value = value.strip()
|
||||
|
||||
if value == 'null':
|
||||
return cls(None)
|
||||
else:
|
||||
raise JSONNullSyntaxError(f'Invalid JSON null value: {value}')
|
||||
|
||||
class JSONArray(JSONType):
|
||||
@property
|
||||
def _type(self):
|
||||
return 'array'
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str):
|
||||
'''
|
||||
Parses a JSON array.
|
||||
|
||||
:param value: The array
|
||||
:type value: str
|
||||
|
||||
:raises JSONArraySyntaxError: If the array is invalid.
|
||||
:raises JSONValueSyntaxError: If a value is invalid
|
||||
:raises JSONObjectSyntaxError: If an object is invalid.
|
||||
:raises JSONStringSyntaxError: If a string is invalid.
|
||||
:raises JSONNullSyntaxError: If a null value is invalid.
|
||||
:raises JSONBooleanSyntaxError: If a null value is invalid.
|
||||
:raises JSONNumberSyntaxError: If a number is invalid.
|
||||
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid.
|
||||
|
||||
:return: The array as a dictionary
|
||||
:rtype: dict
|
||||
'''
|
||||
|
||||
value = value.strip()
|
||||
|
||||
value_type = parse_type(value)
|
||||
if value_type != JSONArray:
|
||||
raise JSONValueSyntaxError('expected an array, got', value_type._type)
|
||||
|
||||
string = False
|
||||
arrays = 0
|
||||
objects = 0
|
||||
current_value = ''
|
||||
escape_sequence = False
|
||||
_array = []
|
||||
|
||||
for c in value[1:-1]:
|
||||
|
||||
if c == '"' and not escape_sequence:
|
||||
string = not string
|
||||
|
||||
if c == '[' and not string:
|
||||
arrays += 1
|
||||
|
||||
if c == ']' and not string:
|
||||
arrays -= 1
|
||||
|
||||
if c == '{' and not string:
|
||||
objects += 1
|
||||
|
||||
if c == '}' and not string:
|
||||
objects -= 1
|
||||
|
||||
current_value += c
|
||||
|
||||
if string and c == '\\':
|
||||
if not escape_sequence:
|
||||
escape_sequence = True
|
||||
else:
|
||||
escape_sequence = False
|
||||
else:
|
||||
escape_sequence = False
|
||||
|
||||
if c == ',' and not string and not arrays and not objects:
|
||||
_array.append(current_value[:-1])
|
||||
current_value = ''
|
||||
|
||||
if current_value:
|
||||
_array.append(current_value)
|
||||
|
||||
array = []
|
||||
for e in _array:
|
||||
array.append(parse_type(e).parse(e).value)
|
||||
|
||||
return cls(array)
|
||||
|
||||
class JSONObject(JSONType):
|
||||
@property
|
||||
def _type(self):
|
||||
return 'object'
|
||||
|
||||
@classmethod
|
||||
def parse(cls, value: str) -> str:
|
||||
'''
|
||||
Parses a JSON object.
|
||||
|
||||
:param value: The value
|
||||
:type value: str
|
||||
|
||||
:raises JSONObjectSyntaxError: If the object is invalid.
|
||||
:raises JSONValueSyntaxError: If a value is invalid
|
||||
:raises JSONArraySyntaxError: If an array is invalid.
|
||||
:raises JSONStringSyntaxError: If a string is invalid.
|
||||
:raises JSONNullSyntaxError: If a null value is invalid.
|
||||
:raises JSONBooleanSyntaxError: If a null value is invalid.
|
||||
:raises JSONNumberSyntaxError: If a number is invalid.
|
||||
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid.
|
||||
|
||||
:return: The object as a dictionary
|
||||
:rtype: dict
|
||||
'''
|
||||
|
||||
value = value.strip()
|
||||
|
||||
value_type = parse_type(value)
|
||||
if value_type != JSONObject:
|
||||
raise JSONValueSyntaxError('expected an object, got', value._type)
|
||||
|
||||
|
||||
string = False
|
||||
current_key = ''
|
||||
current_value = None
|
||||
arrays = 0
|
||||
objects = 0
|
||||
escape_sequence = False
|
||||
_object = {}
|
||||
|
||||
for c in value[1:-1]:
|
||||
|
||||
if c == '"' and not escape_sequence:
|
||||
string = not string
|
||||
|
||||
if c == '[' and not string:
|
||||
arrays += 1
|
||||
|
||||
if c == ']' and not string:
|
||||
arrays -= 1
|
||||
|
||||
if c == '{' and not string:
|
||||
objects += 1
|
||||
|
||||
if c == '}' and not string:
|
||||
objects -= 1
|
||||
|
||||
if current_value is None:
|
||||
current_key += c
|
||||
else:
|
||||
current_value += c
|
||||
|
||||
if string and c == '\\':
|
||||
if not escape_sequence:
|
||||
escape_sequence = True
|
||||
else:
|
||||
escape_sequence = False
|
||||
else:
|
||||
escape_sequence = False
|
||||
|
||||
if not string and c == ':' and not arrays and not objects:
|
||||
current_key = current_key[:-1]
|
||||
current_value = ''
|
||||
|
||||
if not string and c == ',' and not arrays and not objects:
|
||||
if current_value is not None:
|
||||
_object[current_key] = current_value[:-1]
|
||||
current_key = ''
|
||||
current_value = None
|
||||
else:
|
||||
raise JSONObjectSyntaxError(f'expected \':\'')
|
||||
|
||||
if current_key:
|
||||
if current_value is not None:
|
||||
_object[current_key] = current_value
|
||||
current_key = ''
|
||||
current_value = None
|
||||
else:
|
||||
raise JSONObjectSyntaxError(f'expected value')
|
||||
|
||||
json_object = {}
|
||||
for k, v in _object.items():
|
||||
json_object[parse_type(k).parse(k).value] = parse_type(v).parse(v).value
|
||||
|
||||
return cls(json_object)
|
||||
|
||||
def parse_escape_sequences(string: str) -> bool:
|
||||
'''
|
||||
Returns the unescaped string.
|
||||
|
||||
:param string: The string to validate
|
||||
:type string: str
|
||||
|
||||
:return: The unescaped string
|
||||
:rtype: str
|
||||
'''
|
||||
|
||||
result = ''
|
||||
unicode_escape_sequence = None
|
||||
escape_sequence = False
|
||||
for c in string:
|
||||
if c == '\\' and not escape_sequence:
|
||||
escape_sequence = True
|
||||
continue
|
||||
|
||||
|
||||
if unicode_escape_sequence is not None:
|
||||
if len(unicode_escape_sequence) == 4:
|
||||
result += chr(int(unicode_escape_sequence, 16))
|
||||
unicode_escape_sequence = None
|
||||
else:
|
||||
if c.lower() not in '0123456789abcdef':
|
||||
raise EscapeSequenceSyntaxError(f'Invalid unicode escape sequence in JSON string: \\u{unicode_escape_sequence}{c}')
|
||||
unicode_escape_sequence += c
|
||||
|
||||
if not escape_sequence and not unicode_escape_sequence:
|
||||
result += c
|
||||
|
||||
if escape_sequence:
|
||||
escape_sequence = False
|
||||
if c not in ('"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u'):
|
||||
raise EscapeSequenceSyntaxError(f'Invalid escape sequence in JSON string: \\{c}')
|
||||
if c == 'u':
|
||||
unicode_escape_sequence = ''
|
||||
else:
|
||||
result += _JSON_ESCAPE_SEQUENCES[c]
|
||||
|
||||
if unicode_escape_sequence:
|
||||
raise EscapeSequenceSyntaxError(f'Invalid unicode escape sequence in JSON string: \\u{unicode_escape_sequence}')
|
||||
return result
|
||||
|
||||
def parse_type(value: str) -> typing.Type[JSONType]:
|
||||
'''
|
||||
Parses the type of the value.
|
||||
|
||||
:param value: The value
|
||||
:type value: str
|
||||
|
||||
:raises JSONValueSyntaxError: If a value is invalid
|
||||
:raises JSONObjectSyntaxError: If an object is invalid.
|
||||
:raises JSONArraySyntaxError: If an array is invalid.
|
||||
:raises JSONStringSyntaxError: If a string is invalid.
|
||||
:raises JSONNullSyntaxError: If a null value is invalid.
|
||||
:raises JSONBooleanSyntaxError: If a null value is invalid.
|
||||
:raises JSONNumberSyntaxError: If a number is invalid.
|
||||
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid.
|
||||
|
||||
:return: The JSON type
|
||||
:rtype: type
|
||||
'''
|
||||
|
||||
value = value.strip()
|
||||
|
||||
if not value:
|
||||
raise JSONValueSyntaxError('the value cannot be empty')
|
||||
|
||||
if value[0] == '{':
|
||||
if value[-1] != '}':
|
||||
raise JSONArraySyntaxError(f'an object has to be closed with \'}}\', but found \'{value[-1]}\'')
|
||||
|
||||
string = False
|
||||
current_key = ''
|
||||
current_value = None
|
||||
arrays = 0
|
||||
objects = 0
|
||||
escape_sequence = False
|
||||
|
||||
for c in value[1:-1]:
|
||||
terminating_quotation_mark = False
|
||||
|
||||
if c == '"' and not escape_sequence:
|
||||
if string:
|
||||
terminating_quotation_mark = True
|
||||
string = not string
|
||||
|
||||
if c == '[' and not string:
|
||||
arrays += 1
|
||||
|
||||
if c == ']' and not string:
|
||||
arrays -= 1
|
||||
if arrays < 0:
|
||||
raise JSONObjectSyntaxError('unexpected char: \']\'')
|
||||
|
||||
if c == '{' and not string:
|
||||
objects += 1
|
||||
|
||||
if c == '}' and not string:
|
||||
objects -= 1
|
||||
if objects < 0:
|
||||
raise JSONObjectSyntaxError('unexpected char: \'}\'')
|
||||
|
||||
if not string and not terminating_quotation_mark and not arrays and not objects:
|
||||
if c.strip().lower() not in '0123456789abcdefghijklmnopqrstuvwxyz,:[]{}.-' + ('"' if escape_sequence else ''):
|
||||
raise JSONObjectSyntaxError(f'unexpected char: \'{c}\'')
|
||||
|
||||
if current_value is None:
|
||||
current_key += c
|
||||
else:
|
||||
current_value += c
|
||||
|
||||
if string and c == '\\':
|
||||
if not escape_sequence:
|
||||
escape_sequence = True
|
||||
else:
|
||||
escape_sequence = False
|
||||
else:
|
||||
escape_sequence = False
|
||||
|
||||
if not string and c == ':' and not arrays and not objects:
|
||||
current_key = current_key[:-1]
|
||||
if current_value is None:
|
||||
current_value = ''
|
||||
else:
|
||||
raise JSONObjectSyntaxError(f'expected \',\'')
|
||||
|
||||
if not string and c == ',' and not arrays and not objects:
|
||||
if current_value is not None:
|
||||
if parse_type(current_key) != JSONString:
|
||||
raise JSONObjectSyntaxError(f'keys have to be strings')
|
||||
parse_type(current_value[:-1])
|
||||
current_key = ''
|
||||
current_value = None
|
||||
else:
|
||||
raise JSONObjectSyntaxError(f'expected \':\'')
|
||||
|
||||
if current_key:
|
||||
if current_value is not None:
|
||||
if parse_type(current_key) != JSONString:
|
||||
raise JSONObjectSyntaxError(f'keys have to be strings')
|
||||
parse_type(current_value)
|
||||
current_key = ''
|
||||
current_value = None
|
||||
else:
|
||||
raise JSONObjectSyntaxError(f'expected value')
|
||||
|
||||
if string:
|
||||
raise JSONStringSyntaxError('unterminated string literal')
|
||||
|
||||
if value[-2] == ',':
|
||||
raise JSONObjectSyntaxError('expected value after \',\'')
|
||||
|
||||
return JSONObject
|
||||
|
||||
elif value[0] == '[':
|
||||
if value[-1] != ']':
|
||||
raise JSONArraySyntaxError(f'an array has to be closed with \']\', but found \'{value[-1]}\'')
|
||||
|
||||
string = False
|
||||
arrays = 0
|
||||
objects = 0
|
||||
current_value = ''
|
||||
escape_sequence = False
|
||||
|
||||
for c in value[1:-1]:
|
||||
terminating_quotation_mark = False
|
||||
|
||||
if c == '"' and not escape_sequence:
|
||||
if string:
|
||||
terminating_quotation_mark = True
|
||||
string = not string
|
||||
|
||||
if c == '[' and not string:
|
||||
arrays += 1
|
||||
|
||||
if c == ']' and not string:
|
||||
arrays -= 1
|
||||
if arrays < 0:
|
||||
raise JSONArraySyntaxError('unexpected char: \']\'')
|
||||
|
||||
if c == '{' and not string:
|
||||
objects += 1
|
||||
|
||||
if c == '}' and not string:
|
||||
objects -= 1
|
||||
if objects < 0:
|
||||
raise JSONArraySyntaxError('unexpected char: \'}\'')
|
||||
|
||||
if (not string) and (not terminating_quotation_mark) and (not arrays) and (not objects):
|
||||
if c.strip().lower() not in '0123456789abcdefghijklmnopqrstuvwxyz,[]{}.-' + ('"' if escape_sequence else ''):
|
||||
raise JSONArraySyntaxError(f'unexpected char: \'{c}\'')
|
||||
|
||||
current_value += c
|
||||
|
||||
if string and c == '\\':
|
||||
if not escape_sequence:
|
||||
escape_sequence = True
|
||||
else:
|
||||
escape_sequence = False
|
||||
else:
|
||||
escape_sequence = False
|
||||
|
||||
if c == ',' and not string and not arrays and not objects:
|
||||
parse_type(current_value[:-1])
|
||||
current_value = ''
|
||||
|
||||
if string:
|
||||
raise JSONStringSyntaxError('unterminated string literal')
|
||||
|
||||
if value[-2] == ',':
|
||||
raise JSONArraySyntaxError('expected value after \',\'')
|
||||
|
||||
if current_value:
|
||||
parse_type(current_value)
|
||||
|
||||
return JSONArray
|
||||
|
||||
elif value[0] == '"': # string
|
||||
if value[0] != '"':
|
||||
raise JSONStringSyntaxError(f'expected \'"\' as first char, got \'{value[0]}\'')
|
||||
if '\n' in value:
|
||||
raise JSONStringSyntaxError(f'line feeds are not allowed inside of strings. Use escape sequences.')
|
||||
if value.count('"') < 2:
|
||||
raise JSONStringSyntaxError('unterminated string literal')
|
||||
if value.count('"') - value.count('\\"') > 2 or value[-1] != '"':
|
||||
raise JSONStringSyntaxError(f'unexpected token: \'{"\"".join(value.split("\"")[2:])}\'')
|
||||
parse_escape_sequences(value)
|
||||
return JSONString
|
||||
|
||||
elif value in ('null', 'false', 'true'): # null or boolean
|
||||
if value == 'null': # null
|
||||
return JSONNull
|
||||
else: # boolean
|
||||
return JSONBoolean
|
||||
|
||||
elif _JSON_NUMBER_RE.match(value): # number
|
||||
try:
|
||||
float(value)
|
||||
return JSONNumber
|
||||
except:
|
||||
raise JSONNumberSyntaxError(f'invalid number: {value}')
|
||||
|
||||
else:
|
||||
raise JSONValueSyntaxError(f'unexpected token: \'{value}\'')
|
||||
|
||||
def parse_json(json: str) -> dict | list | str | int | float | None | bool:
|
||||
'''
|
||||
Parses the JSON.
|
||||
|
||||
:param json: The JSON data
|
||||
:type json: str
|
||||
|
||||
:raises JSONValueSyntaxError: If a value is invalid
|
||||
:raises JSONObjectSyntaxError: If an object is invalid.
|
||||
:raises JSONArraySyntaxError: If an array is invalid.
|
||||
:raises JSONStringSyntaxError: If a string is invalid.
|
||||
:raises JSONNullSyntaxError: If a null value is invalid.
|
||||
:raises JSONBooleanSyntaxError: If a null value is invalid.
|
||||
:raises JSONNumberSyntaxError: If a number is invalid.
|
||||
:raises EscapeSequenceSyntaxError: If an escape sequence is invalid.
|
||||
|
||||
:return: The JSON as a Python object (``dict``, ``list``, ``str``, ``int``/``float``, ``boolean``, ``None``)
|
||||
:rtype: dict | list | str | int | float | boolean | None
|
||||
'''
|
||||
|
||||
json_type = parse_type(json)
|
||||
return json_type.parse(json).value
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
'''
|
||||
The serializers
|
||||
|
||||
Modules:
|
||||
- ini: INI serializer.
|
||||
- json: JSON serializer.
|
||||
'''
|
||||
|
||||
from . import json
|
||||
from . import ini
|
||||
|
||||
__all__ = [
|
||||
'json',
|
||||
'ini'
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
__all__ = [
|
||||
'serialize'
|
||||
]
|
||||
|
||||
def serialize(data: dict, separator: str) -> str:
|
||||
'''
|
||||
Serializes the data to an INI formatted string.
|
||||
|
||||
:param data: The INI configuration.
|
||||
:type data: dict
|
||||
|
||||
:param separator: The separator.
|
||||
:type separator: str
|
||||
|
||||
:return: The INI formatted string.
|
||||
:rtype: str
|
||||
'''
|
||||
|
||||
result = ''
|
||||
for key, value in data.items():
|
||||
if hasattr(value, '_name'):
|
||||
result += value.to_string(separator=separator)
|
||||
else:
|
||||
result += f'{key}{separator}{value}\n'
|
||||
return result.strip()
|
||||
@@ -0,0 +1,152 @@
|
||||
# Copyright 2026 jCloud Services GbR
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://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 copy
|
||||
from typing import *
|
||||
import collections.abc
|
||||
|
||||
__all__ = [
|
||||
'serialize'
|
||||
]
|
||||
|
||||
_ESCAPE_SEQUENCES = {
|
||||
'\"': r'\"',
|
||||
'\\': r'\\',
|
||||
'\b': r'\b',
|
||||
'\f': r'\f',
|
||||
'\n': r'\n',
|
||||
'\r': r'\r',
|
||||
'\t': r'\t',
|
||||
}
|
||||
|
||||
class _Mapper:
|
||||
def __init__(self, mapping = {}):
|
||||
self._mapping = copy.deepcopy(mapping)
|
||||
|
||||
def _set(self, key, value):
|
||||
self._mapping[key] = value
|
||||
|
||||
def set(self, key, value):
|
||||
self._set(key, value)
|
||||
return self
|
||||
|
||||
def map(self, value):
|
||||
return self._mapping[value]
|
||||
|
||||
_MAPPER = _Mapper().set(True, 'true').set(False, 'false').set(None, 'null')
|
||||
|
||||
def _serialize_object(data: dict, level: int, indent: int, indent_char: str, separators: Tuple[str, str]) -> str:
|
||||
result = '{'
|
||||
_count = 0
|
||||
for _key, value in data.items():
|
||||
_count += 1
|
||||
key = _serialize(_key, 0, 0, '', separators)
|
||||
value = _serialize(value, level + 1, indent, indent_char, separators)
|
||||
if not isinstance(_key, str):
|
||||
key = '"' + key + '"'
|
||||
if indent:
|
||||
result += '\n'
|
||||
result += (level + 1) * indent * indent_char
|
||||
result += key
|
||||
result += separators[1]
|
||||
result += value
|
||||
if _count != len(data.keys()):
|
||||
if indent:
|
||||
result += separators[0].strip()
|
||||
else:
|
||||
result += separators[0]
|
||||
if indent:
|
||||
result += '\n'
|
||||
result += level * indent * indent_char + '}'
|
||||
return result
|
||||
|
||||
def _serialize_array(data: Sequence, level: int, indent: int, indent_char: str, separators: Tuple[str, str]) -> str:
|
||||
result = '['
|
||||
_count = 0
|
||||
for value in data:
|
||||
_count += 1
|
||||
value = _serialize(value, level + 1, indent, indent_char, separators)
|
||||
if indent:
|
||||
result += '\n'
|
||||
result += (level + 1) * indent * indent_char
|
||||
result += value
|
||||
if _count != len(data):
|
||||
if indent:
|
||||
result += separators[0].strip()
|
||||
else:
|
||||
result += separators[0]
|
||||
if indent:
|
||||
result += '\n'
|
||||
result += level * indent * indent_char + ']'
|
||||
return result
|
||||
|
||||
def _serialize_string(data: str) -> str:
|
||||
_data = data
|
||||
data = ''
|
||||
for c in _data:
|
||||
data += _ESCAPE_SEQUENCES.get(c, c)
|
||||
return '"' + data + '"'
|
||||
|
||||
def _serialize_number(data: int | float) -> str:
|
||||
return str(data)
|
||||
|
||||
def _serialize(data: dict | list | str | int | float | bool | None, level: int, indent: int, indent_char: str, separators: Tuple[str, str]) -> str:
|
||||
if isinstance(data, (bool, type(None))):
|
||||
return _MAPPER.map(data)
|
||||
elif isinstance(data, dict):
|
||||
return _serialize_object(data, level, indent, indent_char, separators)
|
||||
elif isinstance(data, str):
|
||||
return _serialize_string(data)
|
||||
elif isinstance(data, (int, float)):
|
||||
return _serialize_number(data)
|
||||
elif isinstance(data, collections.abc.Sequence):
|
||||
return _serialize_array(data, level, indent, indent_char, separators)
|
||||
else:
|
||||
raise TypeError(f'Object of type {type(data).__name__} is not JSON serializable')
|
||||
|
||||
def _char_whitespaces(whitespaces: Tuple[int | str, int | str] | int | str) -> Tuple[str, str]:
|
||||
if isinstance(whitespaces, collections.abc.Sequence) and not isinstance(whitespaces, str):
|
||||
if len(whitespaces) != 2:
|
||||
raise ValueError(f'expected exactly two elements in whitespaces, got {len(whitespaces)}')
|
||||
if isinstance(whitespaces, (str, int)):
|
||||
whitespaces = whitespaces, whitespaces
|
||||
if isinstance(whitespaces[0], int):
|
||||
whitespaces = whitespaces[0] * ' ', whitespaces[1]
|
||||
if isinstance(whitespaces[1], int):
|
||||
whitespaces = whitespaces[0], whitespaces[1] * ' '
|
||||
return whitespaces
|
||||
|
||||
def serialize(data: dict | list | str | int | float | bool | None, indent: int = 0, indent_char: str = ' ', separators: Tuple[str, str] = (', ', ': ')) -> str:
|
||||
'''
|
||||
Serializes the data to a JSON formatted string.
|
||||
|
||||
:param data: The data.
|
||||
:type data: dict | list | str | int | float | bool | None
|
||||
:param indent: The indentation
|
||||
:type indent: int
|
||||
:param indent_char: The character the indentation will be filled with
|
||||
:type indent_char: str
|
||||
:param separators: A tuple with the separators. The first string is the element separator and the second string is the key value separator.
|
||||
:type separators: tuple
|
||||
|
||||
:raises TypeError: If the type is not JSON serializable.
|
||||
|
||||
:return: The JSON formatted string.
|
||||
:rtype: str
|
||||
'''
|
||||
|
||||
if len(separators) != 2:
|
||||
raise ValueError(f'expected exactly two elements in separators, got {len(separators)}.')
|
||||
|
||||
return _serialize(data, 0, indent, indent_char, separators)
|
||||
Reference in New Issue
Block a user