updates
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,97 @@
|
||||
# This file is part of py-serializable
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (c) Paul Horton. All Rights Reserved.
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from re import compile as re_compile
|
||||
from typing import Type
|
||||
|
||||
|
||||
class BaseNameFormatter(ABC):
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def encode(cls, property_name: str) -> str:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def decode(cls, property_name: str) -> str:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def decode_as_class_name(cls, name: str) -> str:
|
||||
name = CamelCasePropertyNameFormatter.encode(cls.decode(property_name=name))
|
||||
return name[:1].upper() + name[1:]
|
||||
|
||||
@classmethod
|
||||
def decode_handle_python_builtins_and_keywords(cls, name: str) -> str:
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def encode_handle_python_builtins_and_keywords(cls, name: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
class CamelCasePropertyNameFormatter(BaseNameFormatter):
|
||||
_ENCODE_PATTERN = re_compile(r'_([a-z])')
|
||||
_DECODE_PATTERN = re_compile(r'(?<!^)(?=[A-Z])')
|
||||
|
||||
@classmethod
|
||||
def encode(cls, property_name: str) -> str:
|
||||
property_name = property_name[:1].lower() + property_name[1:]
|
||||
return cls.encode_handle_python_builtins_and_keywords(
|
||||
CamelCasePropertyNameFormatter._ENCODE_PATTERN.sub(lambda x: x.group(1).upper(), property_name)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, property_name: str) -> str:
|
||||
return cls.decode_handle_python_builtins_and_keywords(
|
||||
CamelCasePropertyNameFormatter._DECODE_PATTERN.sub('_', property_name).lower()
|
||||
)
|
||||
|
||||
|
||||
class KebabCasePropertyNameFormatter(BaseNameFormatter):
|
||||
_ENCODE_PATTERN = re_compile(r'(_)')
|
||||
|
||||
@classmethod
|
||||
def encode(cls, property_name: str) -> str:
|
||||
property_name = cls.encode_handle_python_builtins_and_keywords(name=property_name)
|
||||
property_name = property_name[:1].lower() + property_name[1:]
|
||||
return KebabCasePropertyNameFormatter._ENCODE_PATTERN.sub(lambda x: '-', property_name)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, property_name: str) -> str:
|
||||
return cls.decode_handle_python_builtins_and_keywords(property_name.replace('-', '_'))
|
||||
|
||||
|
||||
class SnakeCasePropertyNameFormatter(BaseNameFormatter):
|
||||
_ENCODE_PATTERN = re_compile(r'(.)([A-Z][a-z]+)')
|
||||
|
||||
@classmethod
|
||||
def encode(cls, property_name: str) -> str:
|
||||
property_name = property_name[:1].lower() + property_name[1:]
|
||||
return cls.encode_handle_python_builtins_and_keywords(
|
||||
SnakeCasePropertyNameFormatter._ENCODE_PATTERN.sub(lambda x: x.group(1).upper(), property_name)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def decode(cls, property_name: str) -> str:
|
||||
return cls.decode_handle_python_builtins_and_keywords(property_name)
|
||||
|
||||
|
||||
class CurrentFormatter:
|
||||
formatter: Type['BaseNameFormatter'] = CamelCasePropertyNameFormatter
|
||||
@@ -0,0 +1,226 @@
|
||||
# This file is part of py-serializable
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (c) Paul Horton. All Rights Reserved.
|
||||
|
||||
from datetime import date, datetime
|
||||
from logging import getLogger
|
||||
from re import compile as re_compile
|
||||
from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, Union
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
from . import ObjectMetadataLibrary, ViewType
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
_logger = getLogger(__name__)
|
||||
|
||||
|
||||
class BaseHelper:
|
||||
"""Base Helper.
|
||||
|
||||
Inherit from this class and implement/override the needed functions!
|
||||
|
||||
This class does not provide any functionality,
|
||||
it is more like a Protocol with some fallback implementations.
|
||||
"""
|
||||
|
||||
# region general/fallback
|
||||
|
||||
@classmethod
|
||||
def serialize(cls, o: Any) -> Union[Any, str]:
|
||||
"""general purpose serializer"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, o: Any) -> Any:
|
||||
"""general purpose deserializer"""
|
||||
raise NotImplementedError()
|
||||
|
||||
# endregion general/fallback
|
||||
|
||||
# region json specific
|
||||
|
||||
@classmethod
|
||||
def json_normalize(cls, o: Any, *,
|
||||
view: Optional[Type['ViewType']],
|
||||
prop_info: 'ObjectMetadataLibrary.SerializableProperty',
|
||||
ctx: Type[Any],
|
||||
**kwargs: Any) -> Optional[Any]:
|
||||
"""json specific normalizer"""
|
||||
return cls.json_serialize(o)
|
||||
|
||||
@classmethod
|
||||
def json_serialize(cls, o: Any) -> Union[str, Any]:
|
||||
"""json specific serializer"""
|
||||
return cls.serialize(o)
|
||||
|
||||
@classmethod
|
||||
def json_denormalize(cls, o: Any, *,
|
||||
prop_info: 'ObjectMetadataLibrary.SerializableProperty',
|
||||
ctx: Type[Any],
|
||||
**kwargs: Any) -> Any:
|
||||
"""json specific denormalizer
|
||||
|
||||
:param tCls: the class that was desired to denormalize to
|
||||
:param pCls: tha prent class - as context
|
||||
"""
|
||||
return cls.json_deserialize(o)
|
||||
|
||||
@classmethod
|
||||
def json_deserialize(cls, o: Any) -> Any:
|
||||
"""json specific deserializer"""
|
||||
return cls.deserialize(o)
|
||||
|
||||
# endregion json specific
|
||||
|
||||
# region xml specific
|
||||
|
||||
@classmethod
|
||||
def xml_normalize(cls, o: Any, *,
|
||||
element_name: str,
|
||||
view: Optional[Type['ViewType']],
|
||||
xmlns: Optional[str],
|
||||
prop_info: 'ObjectMetadataLibrary.SerializableProperty',
|
||||
ctx: Type[Any],
|
||||
**kwargs: Any) -> Optional[Union['Element', Any]]:
|
||||
"""xml specific normalizer"""
|
||||
return cls.xml_serialize(o)
|
||||
|
||||
@classmethod
|
||||
def xml_serialize(cls, o: Any) -> Union[str, Any]:
|
||||
"""xml specific serializer"""
|
||||
return cls.serialize(o)
|
||||
|
||||
@classmethod
|
||||
def xml_denormalize(cls, o: 'Element', *,
|
||||
default_ns: Optional[str],
|
||||
prop_info: 'ObjectMetadataLibrary.SerializableProperty',
|
||||
ctx: Type[Any],
|
||||
**kwargs: Any) -> Any:
|
||||
"""xml specific denormalizer"""
|
||||
return cls.xml_deserialize(o.text)
|
||||
|
||||
@classmethod
|
||||
def xml_deserialize(cls, o: Union[str, Any]) -> Any:
|
||||
"""xml specific deserializer"""
|
||||
return cls.deserialize(o)
|
||||
|
||||
# endregion xml specific
|
||||
|
||||
|
||||
class Iso8601Date(BaseHelper):
|
||||
_PATTERN_DATE = '%Y-%m-%d'
|
||||
|
||||
@classmethod
|
||||
def serialize(cls, o: Any) -> str:
|
||||
if isinstance(o, date):
|
||||
return o.strftime(Iso8601Date._PATTERN_DATE)
|
||||
|
||||
raise ValueError(f'Attempt to serialize a non-date: {o.__class__}')
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, o: Any) -> date:
|
||||
try:
|
||||
return date.fromisoformat(str(o))
|
||||
except ValueError:
|
||||
raise ValueError(f'Date string supplied ({o}) does not match either "{Iso8601Date._PATTERN_DATE}"')
|
||||
|
||||
|
||||
class XsdDate(BaseHelper):
|
||||
|
||||
@classmethod
|
||||
def serialize(cls, o: Any) -> str:
|
||||
if isinstance(o, date):
|
||||
return o.isoformat()
|
||||
|
||||
raise ValueError(f'Attempt to serialize a non-date: {o.__class__}')
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, o: Any) -> date:
|
||||
try:
|
||||
v = str(o)
|
||||
if v.startswith('-'):
|
||||
# Remove any leading hyphen
|
||||
v = v[1:]
|
||||
|
||||
if v.endswith('Z'):
|
||||
v = v[:-1]
|
||||
_logger.warning(
|
||||
'Potential data loss will occur: dates with timezones not supported in Python',
|
||||
stacklevel=2)
|
||||
if '+' in v:
|
||||
v = v[:v.index('+')]
|
||||
_logger.warning(
|
||||
'Potential data loss will occur: dates with timezones not supported in Python',
|
||||
stacklevel=2)
|
||||
return date.fromisoformat(v)
|
||||
except ValueError:
|
||||
raise ValueError(f'Date string supplied ({o}) is not a supported ISO Format')
|
||||
|
||||
|
||||
class XsdDateTime(BaseHelper):
|
||||
|
||||
@staticmethod
|
||||
def __fix_tz(dt: datetime) -> datetime:
|
||||
"""
|
||||
Fix for Python's violation of ISO8601: :py:meth:`datetime.isoformat()` might omit the time offset when in doubt,
|
||||
but the ISO-8601 assumes local time zone.
|
||||
Anyway, the time offset is mandatory for this purpose.
|
||||
"""
|
||||
return dt.astimezone() \
|
||||
if dt.tzinfo is None \
|
||||
else dt
|
||||
|
||||
@classmethod
|
||||
def serialize(cls, o: Any) -> str:
|
||||
if isinstance(o, datetime):
|
||||
return cls.__fix_tz(o).isoformat()
|
||||
|
||||
raise ValueError(f'Attempt to serialize a non-date: {o.__class__}')
|
||||
|
||||
# region fixup_microseconds
|
||||
# see https://github.com/madpah/serializable/pull/138
|
||||
|
||||
__PATTERN_FRACTION = re_compile(r'\.\d+')
|
||||
|
||||
@classmethod
|
||||
def __fix_microseconds(cls, v: str) -> str:
|
||||
"""
|
||||
Fix for Python's violation of ISO8601 for :py:meth:`datetime.fromisoformat`.
|
||||
1. Ensure either 0 or exactly 6 decimal places for seconds.
|
||||
Background: py<3.11 supports either 6 or 0 digits for milliseconds when parsing.
|
||||
2. Ensure correct rounding of microseconds on the 6th digit.
|
||||
"""
|
||||
return cls.__PATTERN_FRACTION.sub(lambda m: f'{(float(m.group(0))):.6f}'[1:], v)
|
||||
|
||||
# endregion fixup_microseconds
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, o: Any) -> datetime:
|
||||
try:
|
||||
v = str(o)
|
||||
if v.startswith('-'):
|
||||
# Remove any leading hyphen
|
||||
v = v[1:]
|
||||
if v.endswith('Z'):
|
||||
# Replace ZULU time with 00:00 offset
|
||||
v = f'{v[:-1]}+00:00'
|
||||
return datetime.fromisoformat(
|
||||
cls.__fix_microseconds(v))
|
||||
except ValueError:
|
||||
raise ValueError(f'Date-Time string supplied ({o}) is not a supported ISO Format')
|
||||
@@ -0,0 +1,20 @@
|
||||
# This file is part of py-serializable
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (c) Paul Horton. All Rights Reserved.
|
||||
|
||||
"""
|
||||
JSON-specific functionality.
|
||||
"""
|
||||
@@ -0,0 +1,2 @@
|
||||
# Marker file for PEP 561. This package uses inline types.
|
||||
# This file is needed to allow other packages to type-check their code against this package.
|
||||
@@ -0,0 +1,80 @@
|
||||
# This file is part of py-serializable
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright (c) Paul Horton. All Rights Reserved.
|
||||
|
||||
"""
|
||||
XML-specific functionality.
|
||||
"""
|
||||
|
||||
__all__ = ['xs_normalizedString', 'xs_token']
|
||||
|
||||
from re import compile as re_compile
|
||||
|
||||
# region normalizedString
|
||||
|
||||
__NORMALIZED_STRING_FORBIDDEN_SEARCH = re_compile(r'\r\n|\t|\n|\r')
|
||||
__NORMALIZED_STRING_FORBIDDEN_REPLACE = ' '
|
||||
|
||||
|
||||
def xs_normalizedString(s: str) -> str:
|
||||
"""Make a ``normalizedString``, adhering XML spec.
|
||||
|
||||
.. epigraph::
|
||||
*normalizedString* represents white space normalized strings.
|
||||
The `·value space· <https://www.w3.org/TR/xmlschema-2/#dt-value-space>`_ of normalizedString is the set of
|
||||
strings that do not contain the carriage return (#xD), line feed (#xA) nor tab (#x9) characters.
|
||||
The `·lexical space· <https://www.w3.org/TR/xmlschema-2/#dt-lexical-space>`_ of normalizedString is the set of
|
||||
strings that do not contain the carriage return (#xD), line feed (#xA) nor tab (#x9) characters.
|
||||
The `·base type· <https://www.w3.org/TR/xmlschema-2/#dt-basetype>`_ of normalizedString is
|
||||
`string <https://www.w3.org/TR/xmlschema-2/#string>`_.
|
||||
|
||||
-- the `XML schema spec <http://www.w3.org/TR/xmlschema-2/#normalizedString>`_
|
||||
"""
|
||||
return __NORMALIZED_STRING_FORBIDDEN_SEARCH.sub(
|
||||
__NORMALIZED_STRING_FORBIDDEN_REPLACE,
|
||||
s)
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region token
|
||||
|
||||
|
||||
__TOKEN_MULTISTRING_SEARCH = re_compile(r' {2,}')
|
||||
__TOKEN_MULTISTRING_REPLACE = ' '
|
||||
|
||||
|
||||
def xs_token(s: str) -> str:
|
||||
"""Make a ``token``, adhering XML spec.
|
||||
|
||||
.. epigraph::
|
||||
*token* represents tokenized strings.
|
||||
The `·value space· <https://www.w3.org/TR/xmlschema-2/#dt-value-space>`_ of token is the set of strings that do
|
||||
not contain the carriage return (#xD), line feed (#xA) nor tab (#x9) characters, that have no leading or
|
||||
trailing spaces (#x20) and that have no internal sequences of two or more spaces.
|
||||
The `·lexical space· <https://www.w3.org/TR/xmlschema-2/#dt-lexical-space>`_ of token is the set of strings that
|
||||
do not contain the carriage return (#xD), line feed (#xA) nor tab (#x9) characters, that have no leading or
|
||||
trailing spaces (#x20) and that have no internal sequences of two or more spaces.
|
||||
The `·base type· <https://www.w3.org/TR/xmlschema-2/#dt-basetype>`_ of token is
|
||||
`normalizedString <https://www.w3.org/TR/xmlschema-2/#normalizedString>`_.
|
||||
|
||||
-- the `XML schema spec <http://www.w3.org/TR/xmlschema-2/#token>`_
|
||||
"""
|
||||
return __TOKEN_MULTISTRING_SEARCH.sub(
|
||||
__TOKEN_MULTISTRING_REPLACE,
|
||||
xs_normalizedString(s).strip())
|
||||
|
||||
# endregion
|
||||
Reference in New Issue
Block a user