You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/libs/knowit/serializer.py

210 lines
6.4 KiB

import datetime
import json
import re
import typing
from datetime import timedelta
from decimal import Decimal
import babelfish
import yaml
from yaml.composer import Composer
from yaml.constructor import SafeConstructor
from yaml.parser import Parser
from yaml.reader import Reader
from yaml.resolver import Resolver as DefaultResolver
from yaml.scanner import Scanner
from knowit.units import units
from knowit.utils import round_decimal
def format_property(profile: str, o):
"""Convert properties to string."""
if isinstance(o, timedelta):
return format_duration(o, profile)
if isinstance(o, babelfish.language.Language):
return format_language(o, profile)
if hasattr(o, 'units'):
return format_quantity(o, profile)
return str(o)
def get_json_encoder(context):
"""Return json encoder that handles all needed object types."""
class StringEncoder(json.JSONEncoder):
"""String json encoder."""
def default(self, o):
return format_property(context['profile'], o)
return StringEncoder
def get_yaml_dumper(context):
"""Return yaml dumper that handles all needed object types."""
class CustomDumper(yaml.SafeDumper):
"""Custom YAML Dumper."""
def default_representer(self, data):
"""Convert data to string."""
if isinstance(data, int):
return self.represent_int(data)
return self.represent_str(str(data))
def default_language_representer(self, data):
"""Convert language to string."""
return self.represent_str(format_language(data, context['profile']))
def default_quantity_representer(self, data):
"""Convert quantity to string."""
return self.default_representer(format_quantity(data, context['profile']))
def default_duration_representer(self, data):
"""Convert quantity to string."""
return self.default_representer(format_duration(data, context['profile']))
CustomDumper.add_representer(babelfish.Language, CustomDumper.default_language_representer)
CustomDumper.add_representer(timedelta, CustomDumper.default_duration_representer)
CustomDumper.add_representer(units.Quantity, CustomDumper.default_quantity_representer)
CustomDumper.add_representer(Decimal, CustomDumper.default_representer)
return CustomDumper
def get_yaml_loader(constructors=None):
"""Return a yaml loader that handles sequences as python lists."""
constructors = constructors or {}
yaml_implicit_resolvers = dict(DefaultResolver.yaml_implicit_resolvers)
class Resolver(DefaultResolver):
"""Custom YAML Resolver."""
Resolver.yaml_implicit_resolvers.clear()
for ch, vs in yaml_implicit_resolvers.items():
Resolver.yaml_implicit_resolvers.setdefault(ch, []).extend(
(tag, regexp) for tag, regexp in vs
if not tag.endswith('float')
)
Resolver.add_implicit_resolver( # regex copied from yaml source
'!decimal',
re.compile(r'''^(?:
[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)?
|\.[0-9_]+(?:[eE][-+][0-9]+)?
|[-+]?[0-9][0-9_]*(?::[0-9]?[0-9])+\.[0-9_]*
|[-+]?\.(?:inf|Inf|INF)
|\.(?:nan|NaN|NAN)
)$''', re.VERBOSE),
list('-+0123456789.')
)
class CustomLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver):
"""Custom YAML Loader."""
def __init__(self, stream):
Reader.__init__(self, stream)
Scanner.__init__(self)
Parser.__init__(self)
Composer.__init__(self)
SafeConstructor.__init__(self)
Resolver.__init__(self)
CustomLoader.add_constructor('tag:yaml.org,2002:seq', yaml.Loader.construct_python_tuple)
for tag, constructor in constructors.items():
CustomLoader.add_constructor(tag, constructor)
def decimal_constructor(loader, node):
value = loader.construct_scalar(node)
return Decimal(value)
CustomLoader.add_constructor('!decimal', decimal_constructor)
return CustomLoader
def format_duration(
duration: datetime.timedelta,
profile='default',
) -> typing.Union[str, Decimal]:
if profile == 'technical':
return str(duration)
seconds = duration.total_seconds()
if profile == 'code':
return round_decimal(
Decimal((duration.days * 86400 + duration.seconds) * 10 ** 6 + duration.microseconds) / 10**6, min_digits=1
)
hours = int(seconds // 3600)
seconds = seconds - (hours * 3600)
minutes = int(seconds // 60)
seconds = int(seconds - (minutes * 60))
if profile == 'human':
if hours > 0:
return f'{hours} hours {minutes:02d} minutes { seconds:02d} seconds'
if minutes > 0:
return f'{minutes} minutes {seconds:02d} seconds'
return f'{seconds} seconds'
return f'{hours}:{minutes:02d}:{seconds:02d}'
def format_language(
language: babelfish.language.Language,
profile: str = 'default',
) -> str:
if profile in ('default', 'human'):
return str(language.name)
return str(language)
def format_quantity(
quantity,
profile='default',
) -> str:
"""Human friendly format."""
if profile == 'code':
return quantity.magnitude
unit = quantity.units
if unit != 'bit':
technical = profile == 'technical'
if unit == 'hertz':
return _format_quantity(quantity.magnitude, unit='Hz', binary=technical, precision=3 if technical else 1)
root_unit = quantity.to_root_units().units
if root_unit == 'bit':
return _format_quantity(quantity.magnitude, binary=technical, precision=3 if technical else 2)
if root_unit == 'bit / second':
return _format_quantity(quantity.magnitude, unit='bps', binary=technical, precision=3 if technical else 1)
return str(quantity)
def _format_quantity(
num,
unit: str = 'B',
binary: bool = False,
precision: int = 2,
) -> str:
if binary:
factor = 1024
affix = 'i'
else:
factor = 1000
affix = ''
for prefix in ('', 'K', 'M', 'G', 'T', 'P', 'E', 'Z'):
if abs(num) < factor:
break
num /= factor
else:
prefix = 'Y'
return f'{num:3.{precision}f} {prefix}{affix}{unit}'
YAMLLoader = get_yaml_loader()