This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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')

View File

@@ -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.
"""

View File

@@ -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.

View File

@@ -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