1
\$\begingroup\$

The following code is part of a real estate data processing API.

It is being invoked by several modules of a software that translates real estate data in-between different formats (ORM models, DOMs, JSON objects, CSV records, etc).

I'd like to have feedback on general code style and possible improvements of the data parsing functions and the Factory class.

"""Factories library to provide data conversion
functions and an abstract base factory.
"""

from datetime import datetime

__all__ = [
    'BOOL_STRINGS',
    'DATE_FORMAT',
    'BuildError',
    'bool_to_str',
    'float_to_decimal',
    'parse_int',
    'parse_float',
    'parse_decimal',
    'parse_bool',
    'parse_str',
    'parse_date',
    'Factory']

BOOL_STRINGS = {
    '1': True, 'T': True, 'TRUE': True, 'Y': True, 'YES': True, 'J': True,
    'JA': True, 'W': True, 'WAHR': True, '0': False, 'F': False,
    'FALSE': False, 'FALSCH': False, 'N': False, 'NO': False, 'NEIN': False}
DATE_FORMAT = '%Y-%m-%d'


class BuildError(Exception):
    """Indicates an error while building inside a Factory.

    BuildErrors should always be handled within the superordinate factory.
    If a BuildError occurs inside a root factory and cannot be resolved,
    a ConversionError should be risen from it and forwarded to the respective
    Interface rather than the BuildError itself.
    """

    def __init__(self, target=None):
        """Sets an optional target, which failed
        to build and derives a message from it.
        """
        if target is not None:
            if isinstance(target, str):
                msg = target
            else:
                self.target = target
                msg = 'Error building: {}.'.format(str(target))
        else:
            msg = None

        super().__init__(msg)


def bool_to_str(val, default=None, true='true', false='false'):
    """Converts a boolean value into a string
    representation defined by @true and @false.

    If @val is None, None will be returned unless @default is boolean,
    in which case @true respectively @false will be returned.
    """

    if val is None:
        if default is True:
            return true
        elif default is False:
            return false

        return default
    elif val:
        return true

    return false


def float_to_decimal(val, digits=2, default=None):
    """Converts a float value into a decimal string.
    If @val is None, @default will be returned.
    """

    if val is not None:
        return '{{0:.{}f}}'.format(digits).format(val)

    return default


def parse_int(val, default=None, rnd=False):
    """Tries to convert the given value into an integer or
    returns @default on failure.

    If @val is a float, it may be rounded by setting @rnd to True.
    """

    if rnd:
        try:
            return round(val)
        except (TypeError, ValueError):
            return default
    else:
        try:
            return int(val)
        except (TypeError, ValueError):
            return default


def parse_float(val, default=None):
    """Tries to convert the given value into a float or
    returns @default on failure.
    """

    try:
        return float(val)
    except ValueError:
        try:
            val = val.replace(',', '.')
        except (AttributeError, TypeError, ValueError):
            return default
        else:
            try:
                return float(val)
            except ValueError:
                return default
    except TypeError:
        return default


def parse_decimal(val, digits=2, default=None):
    """Tries to convert the given value into a decimal string or
    returns @default on failure.
    """

    return float_to_decimal(
        parse_float(val, default=None), digits=digits, default=default)


def parse_bool(val, default=None):
    """Tries to convert the given value into a boolean or
    returns @default on failure.
    """

    if val in (True, False) or isinstance(val, (float, int)):
        return bool(val)

    return BOOL_STRINGS.get(str(val).upper(), default)


def parse_str(val, default=None, minlen=None, maxlen=None, crop=True):
    """Tries to convert the given value into a string or
    returns @default on failure.

    Optionally, @minlen and @maxlen constraints of the
    resulting string may be specified.

    If so, the resulting string will be cropped to the maximum
    length if it exceeds it unless @crop is False-ish.

    If @crop is a string, the resulting cropped string
    will be terminated with it.
    """

    if val is None:
        return default
    else:
        string = str(val)

        if not minlen and not maxlen:
            return string

        length = len(string)

        if minlen is None or length >= minlen:
            if maxlen is None or length <= maxlen:
                return string

            if crop:
                if isinstance(crop, str):
                    return string[0:maxlen - len(crop)] + crop

                return string[0:maxlen]

        return default


def parse_date(val, default=None, frmt=DATE_FORMAT):
    """Tries to parse a date from a given
    value and returns @default on failure.
    """

    try:
        return datetime.strptime(val, frmt)
    except (TypeError, ValueError):
        return default


class Factory():
    """A basic, abstract factory to build (real estate) data."""

    def __init__(self, master):
        """Initializes the factory from its master.

        @master may be a superordinate Factory or an Interface.
        """
        self.logger = master.logger.inherit(self.__class__.__name__)
        self.temporary_data_context = master.temporary_data_context

    def __call__(self, *args, **kwargs):
        """Redirects to the factory's build method.

        This method should be used to invoke the Factory's build()
        method instead of calling it directly since Factory should
        behave like a simple function.
        """
        return self.build(*args, **kwargs)

    def mktemp(self, mode='w+b', encoding=None, newline=None, suffix=""):
        """Delegates the temporary file creation to
        the temporary data context.
        """
        return self.temporary_data_context.mktemp(
            mode=mode, encoding=encoding, newline=newline, suffix=suffix)

    def mkdtemp(self, suffix=''):
        """Delegates the temporary directory creation to
        the temporary data context.
        """
        return self.temporary_data_context.mkdtemp(suffix=suffix)

    def build(self, *_, **__):
        """Abstract template for the build method."""
        raise NotImplementedError()
\$\endgroup\$
  • \$\begingroup\$ Very clean and high-quality code, not sure if there is anything major to fix, just some minor improvements. But, let's see. \$\endgroup\$ – alecxe Aug 24 '17 at 15:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.