<commit_before># size.py
# Python module to represent storage sizes
#
# Copyright (C) 2010  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): David Cantrell <dcantrell@redhat.com>

import re
import string
import locale
from collections import namedtuple

from decimal import Decimal
from decimal import InvalidOperation
from decimal import ROUND_DOWN
import six

from .errors import SizePlacesError
from .i18n import _, N_
from .util import stringize, unicodeize

# Container for size unit prefix information
_Prefix = namedtuple("Prefix", ["factor", "prefix", "abbr"])

_DECIMAL_FACTOR = 10 ** 3
_BINARY_FACTOR = 2 ** 10

_BYTES_SYMBOL = N_(b"B")
_BYTES_WORDS = (N_(b"bytes"), N_(b"byte"))

# Decimal prefixes for different size increments, along with the name
# and accepted abbreviation for the prefix.  These prefixes are all
# for 'bytes'.
_DECIMAL_PREFIXES = [
   _Prefix(_DECIMAL_FACTOR ** 1, N_(b"kilo"), N_(b"k")),
   _Prefix(_DECIMAL_FACTOR ** 2, N_(b"mega"), N_(b"M")),
   _Prefix(_DECIMAL_FACTOR ** 3, N_(b"giga"), N_(b"G")),
   _Prefix(_DECIMAL_FACTOR ** 4, N_(b"tera"), N_(b"T")),
   _Prefix(_DECIMAL_FACTOR ** 5, N_(b"peta"), N_(b"P")),
   _Prefix(_DECIMAL_FACTOR ** 6, N_(b"exa"), N_(b"E")),
   _Prefix(_DECIMAL_FACTOR ** 7, N_(b"zetta"), N_(b"Z")),
   _Prefix(_DECIMAL_FACTOR ** 8, N_(b"yotta"), N_(b"Y"))
]

# Binary prefixes for the different size increments.  Same structure
# as the above list.
_BINARY_PREFIXES = [
   _Prefix(_BINARY_FACTOR ** 1, N_(b"kibi"), N_(b"Ki")),
   _Prefix(_BINARY_FACTOR ** 2, N_(b"mebi"), N_(b"Mi")),
   _Prefix(_BINARY_FACTOR ** 3, N_(b"gibi"), N_(b"Gi")),
   _Prefix(_BINARY_FACTOR ** 4, N_(b"tebi"), N_(b"Ti")),
   _Prefix(_BINARY_FACTOR ** 5, N_(b"pebi"), N_(b"Pi")),
   _Prefix(_BINARY_FACTOR ** 6, N_(b"exbi"), N_(b"Ei")),
   _Prefix(_BINARY_FACTOR ** 7, N_(b"zebi"), N_(b"Zi")),
   _Prefix(_BINARY_FACTOR ** 8, N_(b"yobi"), N_(b"Yi"))
]

# Empty prefix works both for decimal and binary
_EMPTY_PREFIX = _Prefix(1, b"", b"")

if six.PY2:
    _ASCIIlower_table = string.maketrans(string.ascii_uppercase, string.ascii_lowercase) # pylint: disable=no-member
else:
    _ASCIIlower_table = str.maketrans(string.ascii_uppercase, string.ascii_lowercase) # pylint: disable=no-member

def _lowerASCII(s):
    """Convert a string to lowercase using only ASCII character definitions.

       :param str s: string to convert
       :returns: lower-cased string
       :rtype: str
    """
    if six.PY2:
        return string.translate(s, _ASCIIlower_table) # pylint: disable=no-member
    else:
        return str.translate(s, _ASCIIlower_table) # pylint: disable=no-member

def _makeSpec(prefix, suffix, xlate, lowercase=True):
    """ Synthesizes a whole word from prefix and suffix.

        :param str prefix: a prefix
        :param str suffixes: a suffix
        :param bool xlate: if True, treat as locale specific
        :param bool lowercase: if True, make all lowercase

        :returns:  whole word
        :rtype: str
    """
    if xlate:
        word = (_(prefix) + _(suffix))
        return word.lower() if lowercase else word
    else:
        word = prefix + suffix
        return _lowerASCII(word) if lowercase else word

def _parseUnits(units, xlate):
    """ Parse a unit specification and return corresponding factor.

        :param units: a units specifier
        :type units: any type of string like object
        :param bool xlate: if True, assume locale specific

        :returns: a numeric factor corresponding to the units, if found
        :rtype: int or NoneType

        Looks first for exact matches for a specifier, but, failing that,
        searches for partial matches for abbreviations.

        Normalizes units to lowercase, e.g., MiB and mib are treated the same.
    """
    if units == "":
        return 1

    if xlate:
        units = units.lower()
    else:
        units = _lowerASCII(units)

    # Search for complete matches
    for factor, prefix, abbr in [_EMPTY_PREFIX] + _BINARY_PREFIXES + _DECIMAL_PREFIXES:
        if units == _makeSpec(abbr, _BYTES_SYMBOL, xlate) or \
           units in (_makeSpec(prefix, s, xlate) for s in _BYTES_WORDS):
            return factor

    # Search for unambiguous partial match among binary abbreviations
    matches = [p for p in _BINARY_PREFIXES if _makeSpec(p.abbr, "", xlate).startswith(units)]
    if len(matches) == 1:
        return matches[0].factor

    return None

def _parseSpec(spec):
    """ Parse string representation of size.

        :param spec: the specification of a size with, optionally, units
        :type spec: any type of string like object
        :returns: numeric value of the specification in bytes
        :rtype: Decimal

        :raises ValueError: if spec is unparseable

        Tries to parse the spec first as English, if that fails, as
        a locale specific string.
    """

    if not spec:
        raise ValueError("invalid size specification", spec)

    # Replace the localized radix character with a .
    radix = locale.nl_langinfo(locale.RADIXCHAR)
    if radix != '.':
        spec = spec.replace(radix, '.')

    # Match the string using only digit/space/not-space, since the
    # string might be non-English and contain non-letter characters
    # that Python doesn't understand as parts of words.
    m = re.match(r'(-?\s*[0-9.]+)\s*([^\s]*)$', spec.strip())
    if not m:
        raise ValueError("invalid size specification", spec)

    try:
        size = Decimal(m.groups()[0])
    except InvalidOperation:
        raise ValueError("invalid size specification", spec)

    specifier = m.groups()[1]

    # First try to parse as English.
    try:
        if six.PY2:
            spec_ascii = str(specifier.decode("ascii"))
        else:
            spec_ascii = bytes(specifier, 'ascii')
    except (UnicodeDecodeError, UnicodeEncodeError):
        # String contains non-ascii characters, so can not be English.
        pass
    else:
        factor = _parseUnits(spec_ascii, False)
        if factor is not None:
            return size * factor

    # No English match found, try localized size specs.
    if six.PY2:
        if isinstance(specifier, unicode):
            spec_local = specifier
        else:
            spec_local = specifier.decode("utf-8")
    else:
        spec_local = specifier

    factor = _parseUnits(spec_local, True)
    if factor is not None:
        return size * factor

    raise ValueError("invalid size specification", spec)

class Size(Decimal):
    """ Common class to represent storage device and filesystem sizes.
        Can handle parsing strings such as 45MB or 6.7GB to initialize
        itself, or can be initialized with a numerical size in bytes.
        Also generates human readable strings to a specified number of
        decimal places.
    """

    def __new__(cls, value=0, context=None):
        """ Initialize a new Size object.  Must pass a bytes or a spec value
            for size. The bytes value is a numerical value for the size
            this object represents, in bytes.  The spec value is a string
            specification of the size using any of the size specifiers in the
            _DECIMAL_PREFIXES or _BINARY_PREFIXES lists combined with a 'b' or
            'B'.  For example, to specify 640 kilobytes, you could pass any of
            these spec parameters:

                "640kb"
                "640 kb"
                "640KB"
                "640 KB"
                "640 kilobytes"

            If you want to use a spec value to represent a bytes value,
            you can use the letter 'b' or 'B' or omit the size specifier.
        """
        if isinstance(value, (six.string_types, bytes)):
            size = _parseSpec(value)
        elif isinstance(value, (six.integer_types, float, Decimal)):
            size = Decimal(value)
        elif isinstance(value, Size):
            size = Decimal(value.convertTo())
        else:
            raise ValueError("invalid value %s for size" % value)

        # drop any partial byte
        size = size.to_integral_value(rounding=ROUND_DOWN)
        self = Decimal.__new__(cls, value=size)
        return self

    # Force str and unicode types since the translated sizespec may be unicode
    def _toString(self):
        return self.humanReadable()

    def __str__(self, eng=False, context=None):
        return stringize(self._toString())

    def __unicode__(self):
        return unicodeize(self._toString())

    def __repr__(self):
        return "Size('%s')" % self

    def __deepcopy__(self, memo):
        return Size(self.convertTo())

    def __add__(self, other, context=None):
        return Size(Decimal.__add__(self, other, context=context))

    # needed to make sum() work with Size arguments
    def __radd__(self, other, context=None):
        return Size(Decimal.__radd__(self, other, context=context))

    def __sub__(self, other, context=None):
        # subtraction is implemented using __add__ and negation, so we'll
        # be getting passed a Size
        return Decimal.__sub__(self, other, context=context)

    def __mul__(self, other, context=None):
        return Size(Decimal.__mul__(self, other, context=context))

    def __div__(self, other, context=None):
        return Size(Decimal.__div__(self, other, context=context))

    def __mod__(self, other, context=None):
        return Size(Decimal.__mod__(self, other, context=context))

    def convertTo(self, spec="", xlate=False):
        """ Return the size in the units indicated by the specifier.

            :param str spec: a units specifier
            :returns: a numeric value in the units indicated by the specifier
            :rtype: Decimal
        """
        divisor = _parseUnits(spec, xlate)
        if divisor:
            return Decimal(self) / Decimal(divisor)

        return None

    def humanReadable(self, max_places=2, strip=True, min_value=1):
        """ Return a string representation of this size with appropriate
            size specifier and in the specified number of decimal places.
            Values are always represented using binary not decimal units.
            For example, if the number of bytes represented by this size
            is 65531, expect the representation to be something like
            64.00 KiB, not 65.53 KB.

            :param max_places: number of decimal places to use, default is 2
            :type max_places: an integer type or NoneType
            :param bool strip: True if trailing zeros are to be stripped.
            :param min_value: Lower bound for value, default is 1.
            :type min_value: A precise numeric type: int, long, or Decimal
            :returns: a representation of the size
            :rtype: str

            If max_places is set to None, all non-zero digits will be shown.
            Otherwise, max_places digits will be shown.

            If strip is True and there is a fractional quantity, trailing
            zeros are removed up to the decimal point.

            min_value sets the smallest value allowed.
            If min_value is 10, then single digits on the lhs of
            the decimal will be avoided if possible. In that case,
            9216 KiB is preferred to 9 MiB. However, 1 B has no alternative.
            If min_value is 1, however, 9 MiB is preferred.
            If min_value is 0.1, then 0.75 GiB is preferred to 768 MiB,
            but 0.05 GiB is still displayed as 51.2 MiB.

            humanReadable() is a function that evaluates to a number which
            represents a range of values. For a constant choice of max_places,
            all ranges are of equal size, and are bisected by the result. So,
            if n.humanReadable() == x U and b is the number of bytes in 1 U,
            and e = 1/2 * 1/(10^max_places) * b, then x - e < n < x + e.
        """
        if max_places is not None and (max_places < 0 or not isinstance(max_places, six.integer_types)):
            raise SizePlacesError("max_places must be None or an non-negative integer value")

        if min_value < 0 or not isinstance(min_value, (six.integer_types, Decimal)):
            raise ValueError("min_value must be a precise positive numeric value.")

        # Find the smallest prefix which will allow a number less than
        # _BINARY_FACTOR * min_value to the left of the decimal point.
        # If the number is so large that no prefix will satisfy this
        # requirement use the largest prefix.
        for factor, _prefix, abbr in itertools.chain([_EMPTY_PREFIX], _xlated_binary_prefixes()):
            newcheck = super(Size, self).__div__(Decimal(factor))

            if abs(newcheck) < _BINARY_FACTOR * min_value:
                # nice value, use this factor, prefix and abbr
                break

        if max_places is not None:
            newcheck = newcheck.quantize(Decimal(10) ** -max_places)

        retval_str = str(newcheck)

        if '.' in retval_str and strip:
            retval_str = retval_str.rstrip("0").rstrip(".")

        # If necessary, substitute with a localized separator before returning
        radix = locale.nl_langinfo(locale.RADIXCHAR)
        if radix != '.':
            retval_str = retval_str.replace('.', radix)

        # pylint: disable=undefined-loop-variable
        return retval_str + " " + abbr + _("B")
<commit_msg>Add xlate parameter to humanReadable().<commit_after># size.py
# Python module to represent storage sizes
#
# Copyright (C) 2010  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): David Cantrell <dcantrell@redhat.com>

import re
import string
import locale
from collections import namedtuple

from decimal import Decimal
from decimal import InvalidOperation
from decimal import ROUND_DOWN
import six

from .errors import SizePlacesError
from .i18n import _, N_
from .util import stringize, unicodeize

# Container for size unit prefix information
_Prefix = namedtuple("Prefix", ["factor", "prefix", "abbr"])

_DECIMAL_FACTOR = 10 ** 3
_BINARY_FACTOR = 2 ** 10

_BYTES_SYMBOL = N_(b"B")
_BYTES_WORDS = (N_(b"bytes"), N_(b"byte"))

# Decimal prefixes for different size increments, along with the name
# and accepted abbreviation for the prefix.  These prefixes are all
# for 'bytes'.
_DECIMAL_PREFIXES = [
   _Prefix(_DECIMAL_FACTOR ** 1, N_(b"kilo"), N_(b"k")),
   _Prefix(_DECIMAL_FACTOR ** 2, N_(b"mega"), N_(b"M")),
   _Prefix(_DECIMAL_FACTOR ** 3, N_(b"giga"), N_(b"G")),
   _Prefix(_DECIMAL_FACTOR ** 4, N_(b"tera"), N_(b"T")),
   _Prefix(_DECIMAL_FACTOR ** 5, N_(b"peta"), N_(b"P")),
   _Prefix(_DECIMAL_FACTOR ** 6, N_(b"exa"), N_(b"E")),
   _Prefix(_DECIMAL_FACTOR ** 7, N_(b"zetta"), N_(b"Z")),
   _Prefix(_DECIMAL_FACTOR ** 8, N_(b"yotta"), N_(b"Y"))
]

# Binary prefixes for the different size increments.  Same structure
# as the above list.
_BINARY_PREFIXES = [
   _Prefix(_BINARY_FACTOR ** 1, N_(b"kibi"), N_(b"Ki")),
   _Prefix(_BINARY_FACTOR ** 2, N_(b"mebi"), N_(b"Mi")),
   _Prefix(_BINARY_FACTOR ** 3, N_(b"gibi"), N_(b"Gi")),
   _Prefix(_BINARY_FACTOR ** 4, N_(b"tebi"), N_(b"Ti")),
   _Prefix(_BINARY_FACTOR ** 5, N_(b"pebi"), N_(b"Pi")),
   _Prefix(_BINARY_FACTOR ** 6, N_(b"exbi"), N_(b"Ei")),
   _Prefix(_BINARY_FACTOR ** 7, N_(b"zebi"), N_(b"Zi")),
   _Prefix(_BINARY_FACTOR ** 8, N_(b"yobi"), N_(b"Yi"))
]

# Empty prefix works both for decimal and binary
_EMPTY_PREFIX = _Prefix(1, b"", b"")

if six.PY2:
    _ASCIIlower_table = string.maketrans(string.ascii_uppercase, string.ascii_lowercase) # pylint: disable=no-member
else:
    _ASCIIlower_table = str.maketrans(string.ascii_uppercase, string.ascii_lowercase) # pylint: disable=no-member

def _lowerASCII(s):
    """Convert a string to lowercase using only ASCII character definitions.

       :param str s: string to convert
       :returns: lower-cased string
       :rtype: str
    """
    if six.PY2:
        return string.translate(s, _ASCIIlower_table) # pylint: disable=no-member
    else:
        return str.translate(s, _ASCIIlower_table) # pylint: disable=no-member

def _makeSpec(prefix, suffix, xlate, lowercase=True):
    """ Synthesizes a whole word from prefix and suffix.

        :param str prefix: a prefix
        :param str suffixes: a suffix
        :param bool xlate: if True, treat as locale specific
        :param bool lowercase: if True, make all lowercase

        :returns:  whole word
        :rtype: str
    """
    if xlate:
        word = (_(prefix) + _(suffix))
        return word.lower() if lowercase else word
    else:
        word = prefix + suffix
        return _lowerASCII(word) if lowercase else word

def _parseUnits(units, xlate):
    """ Parse a unit specification and return corresponding factor.

        :param units: a units specifier
        :type units: any type of string like object
        :param bool xlate: if True, assume locale specific

        :returns: a numeric factor corresponding to the units, if found
        :rtype: int or NoneType

        Looks first for exact matches for a specifier, but, failing that,
        searches for partial matches for abbreviations.

        Normalizes units to lowercase, e.g., MiB and mib are treated the same.
    """
    if units == "":
        return 1

    if xlate:
        units = units.lower()
    else:
        units = _lowerASCII(units)

    # Search for complete matches
    for factor, prefix, abbr in [_EMPTY_PREFIX] + _BINARY_PREFIXES + _DECIMAL_PREFIXES:
        if units == _makeSpec(abbr, _BYTES_SYMBOL, xlate) or \
           units in (_makeSpec(prefix, s, xlate) for s in _BYTES_WORDS):
            return factor

    # Search for unambiguous partial match among binary abbreviations
    matches = [p for p in _BINARY_PREFIXES if _makeSpec(p.abbr, "", xlate).startswith(units)]
    if len(matches) == 1:
        return matches[0].factor

    return None

def _parseSpec(spec):
    """ Parse string representation of size.

        :param spec: the specification of a size with, optionally, units
        :type spec: any type of string like object
        :returns: numeric value of the specification in bytes
        :rtype: Decimal

        :raises ValueError: if spec is unparseable

        Tries to parse the spec first as English, if that fails, as
        a locale specific string.
    """

    if not spec:
        raise ValueError("invalid size specification", spec)

    # Replace the localized radix character with a .
    radix = locale.nl_langinfo(locale.RADIXCHAR)
    if radix != '.':
        spec = spec.replace(radix, '.')

    # Match the string using only digit/space/not-space, since the
    # string might be non-English and contain non-letter characters
    # that Python doesn't understand as parts of words.
    m = re.match(r'(-?\s*[0-9.]+)\s*([^\s]*)$', spec.strip())
    if not m:
        raise ValueError("invalid size specification", spec)

    try:
        size = Decimal(m.groups()[0])
    except InvalidOperation:
        raise ValueError("invalid size specification", spec)

    specifier = m.groups()[1]

    # First try to parse as English.
    try:
        if six.PY2:
            spec_ascii = str(specifier.decode("ascii"))
        else:
            spec_ascii = bytes(specifier, 'ascii')
    except (UnicodeDecodeError, UnicodeEncodeError):
        # String contains non-ascii characters, so can not be English.
        pass
    else:
        factor = _parseUnits(spec_ascii, False)
        if factor is not None:
            return size * factor

    # No English match found, try localized size specs.
    if six.PY2:
        if isinstance(specifier, unicode):
            spec_local = specifier
        else:
            spec_local = specifier.decode("utf-8")
    else:
        spec_local = specifier

    factor = _parseUnits(spec_local, True)
    if factor is not None:
        return size * factor

    raise ValueError("invalid size specification", spec)

class Size(Decimal):
    """ Common class to represent storage device and filesystem sizes.
        Can handle parsing strings such as 45MB or 6.7GB to initialize
        itself, or can be initialized with a numerical size in bytes.
        Also generates human readable strings to a specified number of
        decimal places.
    """

    def __new__(cls, value=0, context=None):
        """ Initialize a new Size object.  Must pass a bytes or a spec value
            for size. The bytes value is a numerical value for the size
            this object represents, in bytes.  The spec value is a string
            specification of the size using any of the size specifiers in the
            _DECIMAL_PREFIXES or _BINARY_PREFIXES lists combined with a 'b' or
            'B'.  For example, to specify 640 kilobytes, you could pass any of
            these spec parameters:

                "640kb"
                "640 kb"
                "640KB"
                "640 KB"
                "640 kilobytes"

            If you want to use a spec value to represent a bytes value,
            you can use the letter 'b' or 'B' or omit the size specifier.
        """
        if isinstance(value, (six.string_types, bytes)):
            size = _parseSpec(value)
        elif isinstance(value, (six.integer_types, float, Decimal)):
            size = Decimal(value)
        elif isinstance(value, Size):
            size = Decimal(value.convertTo())
        else:
            raise ValueError("invalid value %s for size" % value)

        # drop any partial byte
        size = size.to_integral_value(rounding=ROUND_DOWN)
        self = Decimal.__new__(cls, value=size)
        return self

    # Force str and unicode types since the translated sizespec may be unicode
    def _toString(self):
        return self.humanReadable()

    def __str__(self, eng=False, context=None):
        return stringize(self._toString())

    def __unicode__(self):
        return unicodeize(self._toString())

    def __repr__(self):
        return "Size('%s')" % self

    def __deepcopy__(self, memo):
        return Size(self.convertTo())

    def __add__(self, other, context=None):
        return Size(Decimal.__add__(self, other, context=context))

    # needed to make sum() work with Size arguments
    def __radd__(self, other, context=None):
        return Size(Decimal.__radd__(self, other, context=context))

    def __sub__(self, other, context=None):
        # subtraction is implemented using __add__ and negation, so we'll
        # be getting passed a Size
        return Decimal.__sub__(self, other, context=context)

    def __mul__(self, other, context=None):
        return Size(Decimal.__mul__(self, other, context=context))

    def __div__(self, other, context=None):
        return Size(Decimal.__div__(self, other, context=context))

    def __mod__(self, other, context=None):
        return Size(Decimal.__mod__(self, other, context=context))

    def convertTo(self, spec="", xlate=False):
        """ Return the size in the units indicated by the specifier.

            :param str spec: a units specifier
            :returns: a numeric value in the units indicated by the specifier
            :rtype: Decimal
        """
        divisor = _parseUnits(spec, xlate)
        if divisor:
            return Decimal(self) / Decimal(divisor)

        return None

    def humanReadable(self, max_places=2, strip=True, min_value=1, xlate=True):
        """ Return a string representation of this size with appropriate
            size specifier and in the specified number of decimal places.
            Values are always represented using binary not decimal units.
            For example, if the number of bytes represented by this size
            is 65531, expect the representation to be something like
            64.00 KiB, not 65.53 KB.

            :param max_places: number of decimal places to use, default is 2
            :type max_places: an integer type or NoneType
            :param bool strip: True if trailing zeros are to be stripped.
            :param min_value: Lower bound for value, default is 1.
            :type min_value: A precise numeric type: int, long, or Decimal
            :param bool xlate: If True, translate for current locale
            :returns: a representation of the size
            :rtype: str

            If max_places is set to None, all non-zero digits will be shown.
            Otherwise, max_places digits will be shown.

            If strip is True and there is a fractional quantity, trailing
            zeros are removed up to the decimal point.

            min_value sets the smallest value allowed.
            If min_value is 10, then single digits on the lhs of
            the decimal will be avoided if possible. In that case,
            9216 KiB is preferred to 9 MiB. However, 1 B has no alternative.
            If min_value is 1, however, 9 MiB is preferred.
            If min_value is 0.1, then 0.75 GiB is preferred to 768 MiB,
            but 0.05 GiB is still displayed as 51.2 MiB.

            humanReadable() is a function that evaluates to a number which
            represents a range of values. For a constant choice of max_places,
            all ranges are of equal size, and are bisected by the result. So,
            if n.humanReadable() == x U and b is the number of bytes in 1 U,
            and e = 1/2 * 1/(10^max_places) * b, then x - e < n < x + e.
        """
        if max_places is not None and (max_places < 0 or not isinstance(max_places, six.integer_types)):
            raise SizePlacesError("max_places must be None or an non-negative integer value")

        if min_value < 0 or not isinstance(min_value, (six.integer_types, Decimal)):
            raise ValueError("min_value must be a precise positive numeric value.")

        # Find the smallest prefix which will allow a number less than
        # _BINARY_FACTOR * min_value to the left of the decimal point.
        # If the number is so large that no prefix will satisfy this
        # requirement use the largest prefix.
        for factor, _prefix, abbr in [_EMPTY_PREFIX] + _BINARY_PREFIXES:
            newcheck = super(Size, self).__div__(Decimal(factor))

            if abs(newcheck) < _BINARY_FACTOR * min_value:
                # nice value, use this factor, prefix and abbr
                break

        if max_places is not None:
            newcheck = newcheck.quantize(Decimal(10) ** -max_places)

        retval_str = str(newcheck)

        if '.' in retval_str and strip:
            retval_str = retval_str.rstrip("0").rstrip(".")

        if xlate:
            radix = locale.nl_langinfo(locale.RADIXCHAR)
            if radix != '.':
                retval_str = retval_str.replace('.', radix)

        # pylint: disable=undefined-loop-variable
        return retval_str + " " + _makeSpec(abbr, _BYTES_SYMBOL, xlate, lowercase=False)
<|endoftext|>


<commit_before>'''
@author: Dallas Fraser
@author: 2015-09-29
@organization: MLSB API
@summary: The views for player stats
'''
import unittest
from sqlalchemy.sql.expression import or_
from flask.ext.restful import Resource, reqparse
from flask import Response
from json import dumps
from api.validators import date_validator
from api import DB
from api.model import Team, Player, roster, Bat, Game
from datetime import datetime, date, time
from pprint import PrettyPrinter
parser = reqparse.RequestParser()
parser.add_argument('year', type=int)
parser.add_argument('league_id', type=int)
parser.add_argument('player_id', type=int)
parser.add_argument('team_id', type=int)

class PlayerStatsAPI(Resource):
    def post(self):
        """
            GET request for Players Stats List
            Route: Route['player_stats']
            Parameters:
                year: the year  (int)
                team_id: the team id (int)
                league_id: the league id (int)
                player_id: the player id (int)
            Returns:
                status: 200
                mimetype: application/json
                data: list of Players
        """
        year = None
        id = None
        response = {'success': True,
                  'message': '',
                  'failures': [],
                  'data': {}}
        args = parser.parse_args()
        if args['player_id']:
            bats = DB.session.query(Bat).filter_by(player_id = args['player_id'])
            players = {}
            for bat in bats:
                if bat.player_id in players:
                    players[bat.player_id][bat.classification] += 1
                    players[bat.player_id]['bats'] += 1
                else:
                    players[bat.player_id] = {'bats': 0,
                                              's': 0,
                                              'd': 0,
                                              'k': 0,
                                              'hr': 0,
                                              'go': 0,
                                              'pf': 0}
                    players[bat.player_id][bat.classification] += 1
                    players[bat.player_id]['bats'] += 1

        else:
            if args['year']:
                year = args['year']
            else:
                year = None
            if args['league_id']:
                id = args['league_id']
            else:
                id = None
            team_id = None
            if args['team_id']:
                team_id = args['team_id']
            t1 = time(0, 0)
            t2 = time(0 , 0)
            if year is not None:
                d1 = date(year, 1, 1)
                t1 = time(0, 0)
                d2 = date(year, 12, 30)
                t2 = time(0 , 0)
            else:
                d1 = date(2014, 1, 1)
                d2 = date(date.today().year, 12, 30)
            start = datetime.combine(d1, t1)
            end = datetime.combine(d2, t2)
            games = DB.session.query(Game).filter(Game.date.between(start, end))
            if id is not None:
                games = games.filter_by(league_id=id)
            if team_id is not None:
                games = games.filter(or_(Game.away_team_id == team_id, Game.home_team_id == team_id))
            players = {}
            print(games)
            for game in games:
                for bat in game.bats:
                    print(bat)
                    player = Player.query.get(bat.player_id)
                    if player.name in players:
                        players[player.name][bat.classification] += 1
                        players[player.name]['bats'] += 1
                    else:
                        players[player.name] = {'bats': 0,
                                                  's': 0,
                                                  'd': 0,
                                                  'k': 0,
                                                  'hr': 0,
                                                  'go': 0,
                                                  'pf': 0}
                        players[player.name][bat.classification] += 1
                        players[player.name]['bats'] += 1
        for key, values in players.items():
            players[key]['avg'] = (players[key]['s'] +\
                                   players[key]['d'] +\
                                   players[key]['hr']) / players[key]['bats']
        response['data'] = players
        return Response(dumps(response), status=200, mimetype="application/json")
<commit_msg>Imports<commit_after>'''
@author: Dallas Fraser
@author: 2015-09-29
@organization: MLSB API
@summary: The views for player stats
'''
from sqlalchemy.sql.expression import or_
from flask.ext.restful import Resource, reqparse
from flask import Response
from json import dumps
from api import DB
from api.model import Team, Player, roster, Bat, Game
from datetime import datetime, date, time
parser = reqparse.RequestParser()
parser.add_argument('year', type=int)
parser.add_argument('league_id', type=int)
parser.add_argument('player_id', type=int)
parser.add_argument('team_id', type=int)

class PlayerStatsAPI(Resource):
    def post(self):
        """
            GET request for Players Stats List
            Route: Route['player_stats']
            Parameters:
                year: the year  (int)
                team_id: the team id (int)
                league_id: the league id (int)
                player_id: the player id (int)
            Returns:
                status: 200
                mimetype: application/json
                data: list of Players
        """
        year = None
        id = None
        response = {'success': True,
                  'message': '',
                  'failures': [],
                  'data': {}}
        args = parser.parse_args()
        if args['player_id']:
            bats = DB.session.query(Bat).filter_by(player_id = args['player_id'])
            players = {}
            for bat in bats:
                if bat.player_id in players:
                    players[bat.player_id][bat.classification] += 1
                    players[bat.player_id]['bats'] += 1
                else:
                    players[bat.player_id] = {'bats': 0,
                                              's': 0,
                                              'd': 0,
                                              'k': 0,
                                              'hr': 0,
                                              'go': 0,
                                              'pf': 0}
                    players[bat.player_id][bat.classification] += 1
                    players[bat.player_id]['bats'] += 1

        else:
            if args['year']:
                year = args['year']
            else:
                year = None
            if args['league_id']:
                id = args['league_id']
            else:
                id = None
            team_id = None
            if args['team_id']:
                team_id = args['team_id']
            t1 = time(0, 0)
            t2 = time(0 , 0)
            if year is not None:
                d1 = date(year, 1, 1)
                t1 = time(0, 0)
                d2 = date(year, 12, 30)
                t2 = time(0 , 0)
            else:
                d1 = date(2014, 1, 1)
                d2 = date(date.today().year, 12, 30)
            start = datetime.combine(d1, t1)
            end = datetime.combine(d2, t2)
            games = DB.session.query(Game).filter(Game.date.between(start, end))
            if id is not None:
                games = games.filter_by(league_id=id)
            if team_id is not None:
                games = games.filter(or_(Game.away_team_id == team_id, Game.home_team_id == team_id))
            players = {}
            print(games)
            for game in games:
                for bat in game.bats:
                    print(bat)
                    player = Player.query.get(bat.player_id)
                    if player.name in players:
                        players[player.name][bat.classification] += 1
                        players[player.name]['bats'] += 1
                    else:
                        players[player.name] = {'bats': 0,
                                                  's': 0,
                                                  'd': 0,
                                                  'k': 0,
                                                  'hr': 0,
                                                  'go': 0,
                                                  'pf': 0}
                        players[player.name][bat.classification] += 1
                        players[player.name]['bats'] += 1
        for key, values in players.items():
            players[key]['avg'] = (players[key]['s'] +\
                                   players[key]['d'] +\
                                   players[key]['hr']) / players[key]['bats']
        response['data'] = players
        return Response(dumps(response), status=200, mimetype="application/json")
<|endoftext|>


<commit_before>import jinja2
import psycopg2
from flask import Flask

app = Flask(__name__)

template = jinja2.Template(open('status.html').read())

conn = psycopg2.connect("dbname=webstatus user=postgres")
cur = conn.cursor()

@app.route("/")
def status():
    web_status = {}

    # Get the most recent status
    cur.execute("SELECT status FROM status LIMIT 1;")
    web_status['internet'] = cur.fetchone()[0]

    # get the time since the last status change
    if web_status['internet']:
        cur.execute("SELECT now() - ts FROM status WHERE status = false ORDER BY ts DESC LIMIT 1;")
    else:
        cur.execute("SELECT now() - ts FROM status WHERE status = true ORDER BY ts DESC LIMIT 1;")
    time = cur.fetchone()

    if time is None:
        web_status['time'] = "forever"
    else:
        web_status['time'] = time[0]

    # Get the number of updates in the last hour
    cur.execute("SELECT count(status) FROM status WHERE ts > NOW() - INTERVAL '1 hour';")
    hour_count = cur.fetchone()[0]

    cur.execute("SELECT count(status) FROM status WHERE status = true AND ts > NOW() - INTERVAL '1 hour';")
    web_status['hour_uptime'] = 100*cur.fetchone()[0] / hour_count

    # Get the number of updates in the last day
    cur.execute("SELECT count(status) FROM status WHERE ts > NOW() - INTERVAL '1 day';")
    day_count = cur.fetchone()[0]
    cur.execute("SELECT count(status) FROM status WHERE status = true AND ts > NOW() - INTERVAL '1 day';")
    web_status['day_uptime'] = 100*cur.fetchone()[0]/day_count

    # Get the number of updates in the last week
    cur.execute("SELECT count(status) FROM status WHERE ts > NOW() - INTERVAL '1 week';")
    week_count = cur.fetchone()[0]
    cur.execute("SELECT count(status) FROM status WHERE status = true AND ts > NOW() - INTERVAL '1 week';")
    web_status['week_uptime'] = 100* cur.fetchone()[0]/week_count

    return template.render(web_status)

if __name__ == "__main__":
    app.run()
<commit_msg>Changed listen address and port for the web service.<commit_after>import jinja2
import psycopg2
from flask import Flask

app = Flask(__name__)

template = jinja2.Template(open('status.html').read())

conn = psycopg2.connect("dbname=webstatus user=postgres")
cur = conn.cursor()

@app.route("/")
def status():
    web_status = {}

    # Get the most recent status
    cur.execute("SELECT status FROM status LIMIT 1;")
    web_status['internet'] = cur.fetchone()[0]

    # get the time since the last status change
    if web_status['internet']:
        cur.execute("SELECT now() - ts FROM status WHERE status = false ORDER BY ts DESC LIMIT 1;")
    else:
        cur.execute("SELECT now() - ts FROM status WHERE status = true ORDER BY ts DESC LIMIT 1;")
    time = cur.fetchone()

    if time is None:
        web_status['time'] = "forever"
    else:
        web_status['time'] = time[0]

    # Get the number of updates in the last hour
    cur.execute("SELECT count(status) FROM status WHERE ts > NOW() - INTERVAL '1 hour';")
    hour_count = cur.fetchone()[0]

    cur.execute("SELECT count(status) FROM status WHERE status = true AND ts > NOW() - INTERVAL '1 hour';")
    web_status['hour_uptime'] = 100*cur.fetchone()[0] / hour_count

    # Get the number of updates in the last day
    cur.execute("SELECT count(status) FROM status WHERE ts > NOW() - INTERVAL '1 day';")
    day_count = cur.fetchone()[0]
    cur.execute("SELECT count(status) FROM status WHERE status = true AND ts > NOW() - INTERVAL '1 day';")
    web_status['day_uptime'] = 100*cur.fetchone()[0]/day_count

    # Get the number of updates in the last week
    cur.execute("SELECT count(status) FROM status WHERE ts > NOW() - INTERVAL '1 week';")
    week_count = cur.fetchone()[0]
    cur.execute("SELECT count(status) FROM status WHERE status = true AND ts > NOW() - INTERVAL '1 week';")
    web_status['week_uptime'] = 100* cur.fetchone()[0]/week_count

    return template.render(web_status)

if __name__ == "__main__":
    app.run(host='0.0.0.0', port='8080')
<|endoftext|>


<commit_before>import collections
import struct
import logging
import threading

from . import objectdictionary
from . import common

logger = logging.getLogger(__name__)

# Command, index, subindex
SDO_STRUCT = struct.Struct("<BHB")

REQUEST_SEGMENT_DOWNLOAD = 0 << 5
REQUEST_DOWNLOAD = 1 << 5
REQUEST_UPLOAD = 2 << 5
REQUEST_SEGMENT_UPLOAD = 3 << 5

RESPONSE_SEGMENT_UPLOAD = 0 << 5
RESPONSE_SEGMENT_DOWNLOAD = 1 << 5
RESPONSE_UPLOAD = 2 << 5
RESPONSE_DOWNLOAD = 3 << 5
RESPONSE_ABORTED = 4 << 5

EXPEDITED = 0x2
SIZE_SPECIFIED = 0x1

class SdoClient(collections.Mapping):
    """Handles communication with an SDO server."""

    def __init__(self, node_id, od):
        #: Node ID
        self.id = node_id
        self.network = None
        self.od = od
        self.response = None
        self.response_received = threading.Condition()

    def on_response(self, can_id, data, timestamp):
        with self.response_received:
            self.response = data
            self.response_received.notify_all()

    def send_request(self, sdo_request):
        retires_left = 5
        while retires_left:
            # Wait for node to respond
            with self.response_received:
                self.response = None
                self.network.send_message(0x600 + self.id, sdo_request)
                self.response_received.wait(0.5)

            if self.response is None:
                retires_left -= 1
            else:
                retires_left = 0

        if self.response is None:
            raise SdoCommunicationError("No SDO response received")
        elif self.response[0] == RESPONSE_ABORTED:
            abort_code, = struct.unpack("<L", self.response[4:8])
            raise SdoAbortedError(abort_code)
        else:
            return self.response

    def upload(self, index, subindex):
        """May be called to manually make a read operation.

        :param int index:
            Index of object to read.
        :param int subindex:
            Sub-index of object to read.

        :return: A data object.
        :rtype: bytearray

        :raises canopen.SdoCommunicationError:
            On unexpected response or timeout.
        :raises canopen.SdoAbortedError:
            When node responds with an error.
        """
        request = SDO_STRUCT.pack(REQUEST_UPLOAD, index, subindex)
        request += b"\x00\x00\x00\x00"
        response = self.send_request(request)
        res_command, res_index, res_subindex = SDO_STRUCT.unpack(response[0:4])
        res_data = response[4:]

        if res_command & 0xE0 != RESPONSE_UPLOAD:
            raise SdoCommunicationError("Unexpected response")

        # Check that the message is for us
        if res_index != index or res_subindex != subindex:
            raise SdoCommunicationError((
                "Node returned a value for 0x{:X}:{:d} instead, "
                "maybe there is another SDO master communicating "
                "on the same SDO channel?").format(res_index, res_subindex))

        expedited = res_command & EXPEDITED
        size_specified = res_command & SIZE_SPECIFIED

        length = None

        if expedited and size_specified:
            # Expedited upload
            length = 4 - ((res_command >> 2) & 0x3)
        elif not expedited:
            # Segmented upload
            if size_specified:
                length, = struct.unpack("<L", res_data)

            request = bytearray(8)
            request[0] = REQUEST_SEGMENT_UPLOAD
            res_data = bytearray()
            while True:
                response = self.send_request(request)
                if response[0] & 0xE0 != RESPONSE_SEGMENT_UPLOAD:
                    raise SdoCommunicationError("Unexpected response")
                last_byte = 8 - ((response[0] >> 1) & 0x7)
                res_data.extend(response[1:last_byte])
                if response[0] & 0x1:
                    break
                request[0] ^= 0x10

        if length is not None and len(res_data) != length:
            res_data = res_data[:length]
        return res_data

    def download(self, index, subindex, data):
        """May be called to manually make a write operation.

        :param int index:
            Index of object to write.
        :param int subindex:
            Sub-index of object to write.
        :param bytes data:
            Data to be written.

        :raises canopen.SdoCommunicationError:
            On unexpected response or timeout.
        :raises canopen.SdoAbortedError:
            When node responds with an error.
        """
        length = len(data)
        command = REQUEST_DOWNLOAD | SIZE_SPECIFIED

        if length <= 4:
            # Expedited download
            command |= EXPEDITED
            command |= (4 - length) << 2
            request = SDO_STRUCT.pack(command, index, subindex)
            request += data.ljust(4, b"\x00")
            response = self.send_request(request)
            if response[0] != RESPONSE_DOWNLOAD:
                raise SdoCommunicationError("Unexpected response")
        else:
            # Segmented download
            request = SDO_STRUCT.pack(command, index, subindex)
            request += struct.pack("<L", length)
            response = self.send_request(request)
            if response[0] != RESPONSE_DOWNLOAD:
                raise SdoCommunicationError("Unexpected response")

            request = bytearray(8)
            request[0] = REQUEST_SEGMENT_DOWNLOAD
            for pos in range(0, length, 7):
                request[1:8] = data[pos:pos + 7]
                if pos + 7 >= length:
                    # No more data after this message
                    request[0] |= 1
                # Specify number of bytes in that do not contain segment data
                request[0] |= (8 - len(request)) << 1
                response = self.send_request(request.ljust(8, b'\x00'))
                # Toggle bit for next request
                request[0] ^= 0x10
                if response[0] & 0xE0 != RESPONSE_SEGMENT_DOWNLOAD:
                    raise SdoCommunicationError("Unexpected response")

    def __getitem__(self, index):
        entry = self.od[index]
        if isinstance(entry, objectdictionary.Variable):
            return Variable(self, entry)
        elif isinstance(entry, objectdictionary.Array):
            return Array(self, entry)
        elif isinstance(entry, objectdictionary.Record):
            return Record(self, entry)

    def __iter__(self):
        return iter(self.od)

    def __len__(self):
        return len(self.od)

    def __contains__(self, key):
        return key in self.od

class Record(collections.Mapping):

    def __init__(self, sdo_node, od):
        self.sdo_node = sdo_node
        self.od = od

    def __getitem__(self, subindex):
        return Variable(self.sdo_node, self.od[subindex])

    def __iter__(self):
        return iter(self.od)

    def __len__(self):
        return len(self.od)

    def __contains__(self, subindex):
        return subindex in self.od

class Array(collections.Mapping):

    def __init__(self, sdo_node, od):
        self.sdo_node = sdo_node
        self.od = od

    def __getitem__(self, subindex):
        return Variable(self.sdo_node, self.od[subindex])

    def __iter__(self):
        return iter(range(1, len(self) + 1))

    def __len__(self):
        return self[0].raw

    def __contains__(self, subindex):
        return 0 <= subindex <= len(self)

class Variable(common.Variable):
    """Access object dictionary variable values using SDO protocol."""

    def __init__(self, sdo_node, od):
        self.sdo_node = sdo_node
        common.Variable.__init__(self, od)

    def get_data(self):
        return self.sdo_node.upload(self.od.index, self.od.subindex)

    def set_data(self, data):
        self.sdo_node.download(self.od.index, self.od.subindex, data)

class SdoError(Exception):
    pass

class SdoAbortedError(SdoError):
    """SDO abort exception."""

    CODES = {
        0x05030000: "SDO toggle bit error",
        0x05040000: "Timeout of transfer communication detected",
        0x05040001: "Unknown SDO command specified",
        0x05040003: "Invalid sequence number",
        0x06010000: "Unsupported access to an object",
        0x06010001: "Attempt to read a write only object",
        0x06010002: "Attempt to write a read only object",
        0x06020000: "Object does not exist",
        0x06040042: "PDO length exceeded",
        0x06060000: "Access failed due to a hardware error",
        0x06070010: "Data type and length code do not match",
        0x06090011: "Subindex does not exist",
        0x06090030: "Value range of parameter exceeded",
        0x060A0023: "Resource not available",
        0x08000000: "General error",
        0x08000021: ("Data can not be transferred or stored to the application "
                     "because of local control"),
        0x08000022: ("Data can not be transferred or stored to the application "
                     "because of the present device state")
    }

    def __init__(self, code):
        #: Abort code
        self.code = code

    def __str__(self):
        text = "Code 0x{:08X}".format(self.code)
        if self.code in self.CODES:
            text = text + ", " + self.CODES[self.code]
        return text

class SdoCommunicationError(SdoError):
    """No or unexpected response from slave."""
<commit_msg>Make SDO timeout handling changeable<commit_after>import collections
import struct
import logging
import threading

from . import objectdictionary
from . import common

logger = logging.getLogger(__name__)

# Command, index, subindex
SDO_STRUCT = struct.Struct("<BHB")

REQUEST_SEGMENT_DOWNLOAD = 0 << 5
REQUEST_DOWNLOAD = 1 << 5
REQUEST_UPLOAD = 2 << 5
REQUEST_SEGMENT_UPLOAD = 3 << 5

RESPONSE_SEGMENT_UPLOAD = 0 << 5
RESPONSE_SEGMENT_DOWNLOAD = 1 << 5
RESPONSE_UPLOAD = 2 << 5
RESPONSE_DOWNLOAD = 3 << 5
RESPONSE_ABORTED = 4 << 5

EXPEDITED = 0x2
SIZE_SPECIFIED = 0x1

class SdoClient(collections.Mapping):
    """Handles communication with an SDO server."""

    #: Max time in seconds to wait for response from server
    RESPONSE_TIMEOUT = 0.5

    #: Max number of request retries before rasing error
    MAX_RETRIES = 5

    def __init__(self, node_id, od):
        #: Node ID
        self.id = node_id
        self.network = None
        self.od = od
        self.response = None
        self.response_received = threading.Condition()

    def on_response(self, can_id, data, timestamp):
        with self.response_received:
            self.response = data
            self.response_received.notify_all()

    def send_request(self, sdo_request):
        retires_left = self.MAX_RETRIES
        while retires_left:
            # Wait for node to respond
            with self.response_received:
                self.response = None
                self.network.send_message(0x600 + self.id, sdo_request)
                self.response_received.wait(self.RESPONSE_TIMEOUT)

            if self.response is None:
                retires_left -= 1
            else:
                retires_left = 0

        if self.response is None:
            raise SdoCommunicationError("No SDO response received")
        elif self.response[0] == RESPONSE_ABORTED:
            abort_code, = struct.unpack("<L", self.response[4:8])
            raise SdoAbortedError(abort_code)
        else:
            return self.response

    def upload(self, index, subindex):
        """May be called to manually make a read operation.

        :param int index:
            Index of object to read.
        :param int subindex:
            Sub-index of object to read.

        :return: A data object.
        :rtype: bytearray

        :raises canopen.SdoCommunicationError:
            On unexpected response or timeout.
        :raises canopen.SdoAbortedError:
            When node responds with an error.
        """
        request = SDO_STRUCT.pack(REQUEST_UPLOAD, index, subindex)
        request += b"\x00\x00\x00\x00"
        response = self.send_request(request)
        res_command, res_index, res_subindex = SDO_STRUCT.unpack(response[0:4])
        res_data = response[4:]

        if res_command & 0xE0 != RESPONSE_UPLOAD:
            raise SdoCommunicationError("Unexpected response")

        # Check that the message is for us
        if res_index != index or res_subindex != subindex:
            raise SdoCommunicationError((
                "Node returned a value for 0x{:X}:{:d} instead, "
                "maybe there is another SDO master communicating "
                "on the same SDO channel?").format(res_index, res_subindex))

        expedited = res_command & EXPEDITED
        size_specified = res_command & SIZE_SPECIFIED

        length = None

        if expedited and size_specified:
            # Expedited upload
            length = 4 - ((res_command >> 2) & 0x3)
        elif not expedited:
            # Segmented upload
            if size_specified:
                length, = struct.unpack("<L", res_data)

            request = bytearray(8)
            request[0] = REQUEST_SEGMENT_UPLOAD
            res_data = bytearray()
            while True:
                response = self.send_request(request)
                if response[0] & 0xE0 != RESPONSE_SEGMENT_UPLOAD:
                    raise SdoCommunicationError("Unexpected response")
                last_byte = 8 - ((response[0] >> 1) & 0x7)
                res_data.extend(response[1:last_byte])
                if response[0] & 0x1:
                    break
                request[0] ^= 0x10

        if length is not None and len(res_data) != length:
            res_data = res_data[:length]
        return res_data

    def download(self, index, subindex, data):
        """May be called to manually make a write operation.

        :param int index:
            Index of object to write.
        :param int subindex:
            Sub-index of object to write.
        :param bytes data:
            Data to be written.

        :raises canopen.SdoCommunicationError:
            On unexpected response or timeout.
        :raises canopen.SdoAbortedError:
            When node responds with an error.
        """
        length = len(data)
        command = REQUEST_DOWNLOAD | SIZE_SPECIFIED

        if length <= 4:
            # Expedited download
            command |= EXPEDITED
            command |= (4 - length) << 2
            request = SDO_STRUCT.pack(command, index, subindex)
            request += data.ljust(4, b"\x00")
            response = self.send_request(request)
            if response[0] != RESPONSE_DOWNLOAD:
                raise SdoCommunicationError("Unexpected response")
        else:
            # Segmented download
            request = SDO_STRUCT.pack(command, index, subindex)
            request += struct.pack("<L", length)
            response = self.send_request(request)
            if response[0] != RESPONSE_DOWNLOAD:
                raise SdoCommunicationError("Unexpected response")

            request = bytearray(8)
            request[0] = REQUEST_SEGMENT_DOWNLOAD
            for pos in range(0, length, 7):
                request[1:8] = data[pos:pos + 7]
                if pos + 7 >= length:
                    # No more data after this message
                    request[0] |= 1
                # Specify number of bytes in that do not contain segment data
                request[0] |= (8 - len(request)) << 1
                response = self.send_request(request.ljust(8, b'\x00'))
                # Toggle bit for next request
                request[0] ^= 0x10
                if response[0] & 0xE0 != RESPONSE_SEGMENT_DOWNLOAD:
                    raise SdoCommunicationError("Unexpected response")

    def __getitem__(self, index):
        entry = self.od[index]
        if isinstance(entry, objectdictionary.Variable):
            return Variable(self, entry)
        elif isinstance(entry, objectdictionary.Array):
            return Array(self, entry)
        elif isinstance(entry, objectdictionary.Record):
            return Record(self, entry)

    def __iter__(self):
        return iter(self.od)

    def __len__(self):
        return len(self.od)

    def __contains__(self, key):
        return key in self.od

class Record(collections.Mapping):

    def __init__(self, sdo_node, od):
        self.sdo_node = sdo_node
        self.od = od

    def __getitem__(self, subindex):
        return Variable(self.sdo_node, self.od[subindex])

    def __iter__(self):
        return iter(self.od)

    def __len__(self):
        return len(self.od)

    def __contains__(self, subindex):
        return subindex in self.od

class Array(collections.Mapping):

    def __init__(self, sdo_node, od):
        self.sdo_node = sdo_node
        self.od = od

    def __getitem__(self, subindex):
        return Variable(self.sdo_node, self.od[subindex])

    def __iter__(self):
        return iter(range(1, len(self) + 1))

    def __len__(self):
        return self[0].raw

    def __contains__(self, subindex):
        return 0 <= subindex <= len(self)

class Variable(common.Variable):
    """Access object dictionary variable values using SDO protocol."""

    def __init__(self, sdo_node, od):
        self.sdo_node = sdo_node
        common.Variable.__init__(self, od)

    def get_data(self):
        return self.sdo_node.upload(self.od.index, self.od.subindex)

    def set_data(self, data):
        self.sdo_node.download(self.od.index, self.od.subindex, data)

class SdoError(Exception):
    pass

class SdoAbortedError(SdoError):
    """SDO abort exception."""

    CODES = {
        0x05030000: "SDO toggle bit error",
        0x05040000: "Timeout of transfer communication detected",
        0x05040001: "Unknown SDO command specified",
        0x05040003: "Invalid sequence number",
        0x06010000: "Unsupported access to an object",
        0x06010001: "Attempt to read a write only object",
        0x06010002: "Attempt to write a read only object",
        0x06020000: "Object does not exist",
        0x06040042: "PDO length exceeded",
        0x06060000: "Access failed due to a hardware error",
        0x06070010: "Data type and length code do not match",
        0x06090011: "Subindex does not exist",
        0x06090030: "Value range of parameter exceeded",
        0x060A0023: "Resource not available",
        0x08000000: "General error",
        0x08000021: ("Data can not be transferred or stored to the application "
                     "because of local control"),
        0x08000022: ("Data can not be transferred or stored to the application "
                     "because of the present device state")
    }

    def __init__(self, code):
        #: Abort code
        self.code = code

    def __str__(self):
        text = "Code 0x{:08X}".format(self.code)
        if self.code in self.CODES:
            text = text + ", " + self.CODES[self.code]
        return text

class SdoCommunicationError(SdoError):
    """No or unexpected response from slave."""
<|endoftext|>


<commit_before>from django.conf import settings

class MaprSettings(object):

    MAPR_MENU_CUSTOM = {
        'gene': {
            'default': ('Gene Symbol', ),
            'all': ('Gene Symbol', 'Gene Identifier', ),
            'label': ('Gene', ),
            'ns': (
                'openmicroscopy.org/mapr/gene',
                'openmicroscopy.org/omero/bulk_annotations'
            ),
        },
        'phenotype': {
            'default': ('Phenotype', ),
            'all': ('Phenotype', 'Phenotype Term Accession', ),
            'label': ('Phenotype', ),
            'ns': (
                'openmicroscopy.org/mapr/phenotype',
                'openmicroscopy.org/omero/bulk_annotations'
            ),
        },
        'compound': {
            'default': ('Compound Name', ),
            'all': ('Compound Name', ),
            'label': ('Compound', ),
            'ns': (
                'openmicroscopy.org/omero/compund',
                'openmicroscopy.org/omero/bulk_annotations'
            ),
        },
        'sirna': {
            'default': ('siRNA Identifier', ),
            'all': ('siRNA Identifier', ),
            'label': ('siRNA', ),
            'ns': (
                'openmicroscopy.org/mapr/sirna',
                'openmicroscopy.org/omero/bulk_annotations'
            ),
        },
    }

    MENU_MAPR = getattr(settings, 'MAPR_MENU_CUSTOM', MAPR_MENU_CUSTOM)

mapr_settings = MaprSettings()
<commit_msg>clean up map namespaces<commit_after>from django.conf import settings

class MaprSettings(object):

    MAPR_MENU_CUSTOM = {
        'gene': {
            'default': ('Gene Symbol', ),
            'all': ('Gene Symbol', 'Gene Identifier', ),
            'label': ('Gene', ),
            'ns': (
                'openmicroscopy.org/mapr/gene',
            ),
        },
        'phenotype': {
            'default': ('Phenotype', ),
            'all': ('Phenotype', 'Phenotype Term Accession', ),
            'label': ('Phenotype', ),
            'ns': (
                'openmicroscopy.org/mapr/phenotype',
            ),
        },
        'compound': {
            'default': ('Compound Name', ),
            'all': ('Compound Name', ),
            'label': ('Compound', ),
            'ns': (
                'openmicroscopy.org/omero/compund',
            ),
        },
        'sirna': {
            'default': ('siRNA Identifier', ),
            'all': ('siRNA Identifier', ),
            'label': ('siRNA', ),
            'ns': (
                'openmicroscopy.org/mapr/sirna',
            ),
        },
    }

    MENU_MAPR = getattr(settings, 'MAPR_MENU_CUSTOM', MAPR_MENU_CUSTOM)

mapr_settings = MaprSettings()
<|endoftext|>


<commit_before># coding=utf-8
# Copyright 2022 The TF-Agents Authors.
#
# 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.

"""Example usage of batched_observer_unbatching."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import reverb

from tf_agents.drivers import py_driver
from tf_agents.environments import parallel_py_environment
from tf_agents.environments import suite_gym
from tf_agents.policies import random_py_policy
from tf_agents.replay_buffers import reverb_utils
from tf_agents.specs import tensor_spec
from tf_agents.utils import batched_observer_unbatching

def collect_random(num_episodes: int, num_envs: int):
    env = parallel_py_environment.ParallelPyEnvironment(
        [lambda: suite_gym.load("CartPole-v0")] * num_envs)

    policy = random_py_policy.RandomPyPolicy(
        env.time_step_spec(), env.action_spec())

    replay_buffer_signature = tensor_spec.from_spec(policy.collect_data_spec)
    replay_buffer_signature = tensor_spec.add_outer_dim(
        replay_buffer_signature)
    table = reverb.Table(
        "experience",
        max_size=100,
        sampler=reverb.selectors.Uniform(),
        remover=reverb.selectors.Fifo(),
        rate_limiter=reverb.rate_limiters.MinSize(1),
        signature=replay_buffer_signature,
    )
    reverb_server = reverb.Server([table])

    def create_add_episode_observer():
        return reverb_utils.ReverbAddEpisodeObserver(
            reverb_server.localhost_client(),
            table_name="experience",
            max_sequence_length=200,
        )

    rb_observer = batched_observer_unbatching.BatchedObserverUnbatching(
        create_add_episode_observer, batch_size=num_envs)

    driver = py_driver.PyDriver(
        env, policy, observers=[rb_observer], max_episodes=num_episodes)
    driver.run(env.reset())
<commit_msg>Add missing doc string.<commit_after># coding=utf-8
# Copyright 2022 The TF-Agents Authors.
#
# 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.

"""Example usage of batched_observer_unbatching."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import reverb

from tf_agents.drivers import py_driver
from tf_agents.environments import parallel_py_environment
from tf_agents.environments import suite_gym
from tf_agents.policies import random_py_policy
from tf_agents.replay_buffers import reverb_utils
from tf_agents.specs import tensor_spec
from tf_agents.utils import batched_observer_unbatching

def collect_random(num_episodes: int, num_envs: int):
    """Collects full episodes of the CartPole-v0 env using a random policy."""
    env = parallel_py_environment.ParallelPyEnvironment(
        [lambda: suite_gym.load("CartPole-v0")] * num_envs)

    policy = random_py_policy.RandomPyPolicy(
        env.time_step_spec(), env.action_spec())

    replay_buffer_signature = tensor_spec.from_spec(policy.collect_data_spec)
    replay_buffer_signature = tensor_spec.add_outer_dim(
        replay_buffer_signature)
    table = reverb.Table(
        "experience",
        max_size=100,
        sampler=reverb.selectors.Uniform(),
        remover=reverb.selectors.Fifo(),
        rate_limiter=reverb.rate_limiters.MinSize(1),
        signature=replay_buffer_signature,
    )
    reverb_server = reverb.Server([table])

    def create_add_episode_observer():
        return reverb_utils.ReverbAddEpisodeObserver(
            reverb_server.localhost_client(),
            table_name="experience",
            max_sequence_length=200,
        )

    rb_observer = batched_observer_unbatching.BatchedObserverUnbatching(
        create_add_episode_observer, batch_size=num_envs)

    driver = py_driver.PyDriver(
        env, policy, observers=[rb_observer], max_episodes=num_episodes)
    driver.run(env.reset())
<|endoftext|>


<commit_before># size.py
# Python module to represent storage sizes
#
# Copyright (C) 2010  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): David Cantrell <dcantrell@redhat.com>

import re
import string
import locale
from collections import namedtuple

from decimal import Decimal
from decimal import InvalidOperation
from decimal import ROUND_DOWN

from .errors import SizeParamsError, SizePlacesError
from .i18n import _, P_, N_

# Container for size unit prefix information
_Prefix = namedtuple("Prefix", ["factor", "prefix", "abbr"])

# Decimal prefixes for different size increments, along with the name
# and accepted abbreviation for the prefix.  These prefixes are all
# for 'bytes'.
_decimalPrefixes = [_Prefix(1000, N_("kilo"), N_("k")),
                    _Prefix(1000**2, N_("mega"), N_("M")),
                    _Prefix(1000**3, N_("giga"), N_("G")),
                    _Prefix(1000**4, N_("tera"), N_("T")),
                    _Prefix(1000**5, N_("peta"), N_("P")),
                    _Prefix(1000**6, N_("exa"), N_("E")),
                    _Prefix(1000**7, N_("zetta"), N_("Z")),
                    _Prefix(1000**8, N_("yotta"), N_("Y"))]

# Binary prefixes for the different size increments.  Same structure
# as the above list.
_binaryPrefixes = [_Prefix(1024, N_("kibi"), N_("Ki")),
                   _Prefix(1024**2, N_("mebi"), N_("Mi")),
                   _Prefix(1024**3, N_("gibi"), N_("Gi")),
                   _Prefix(1024**4, N_("tebi"), N_("Ti")),
                   _Prefix(1024**5, N_("pebi"), N_("Pi")),
                   _Prefix(1024**6, N_("exbi"), N_("Ei")),
                   _Prefix(1024**7, N_("zebi"), N_("Zi")),
                   _Prefix(1024**8, N_("yobi"), N_("Yi"))]

_bytes = [N_('B'), N_('b'), N_('byte'), N_('bytes')]
_prefixes = _binaryPrefixes + _decimalPrefixes

# Translated versions of the byte and prefix arrays
# All strings are decoded as utf-8 so that locale-specific upper/lower functions work
def _xlated_bytes():
    """Return a translated version of the bytes list as a list of unicode strings"""
    return [_(b).decode("utf-8") for b in _bytes]

def _xlated_binary_prefixes():
    return (_Prefix(p.factor, _(p.prefix).decode("utf-8"), _(p.abbr).decode("utf-8")) \
            for p in _binaryPrefixes)

def _xlated_prefixes():
    """Return translated prefixes as unicode strings"""
    xlated_binary = list(_xlated_binary_prefixes())
    xlated_decimal = [_Prefix(p.factor, _(p.prefix).decode("utf-8"), _(p.abbr).decode("utf-8")) \
                      for p in _decimalPrefixes]

    return xlated_binary + xlated_decimal

_ASCIIlower_table = string.maketrans(string.ascii_uppercase, string.ascii_lowercase)
def _lowerASCII(s):
    """Convert a string to lowercase using only ASCII character definitions."""
    return string.translate(s, _ASCIIlower_table)

def _makeSpecs(prefix, abbr, xlate):
    """ Internal method used to generate a list of specifiers. """
    specs = []

    if prefix:
        if xlate:
            specs.append(prefix.lower() + _("byte").decode("utf-8"))
            specs.append(prefix.lower() + _("bytes").decode("utf-8"))
        else:
            specs.append(_lowerASCII(prefix) + "byte")
            specs.append(_lowerASCII(prefix) + "bytes")

    if abbr:
        if xlate:
            specs.append(abbr.lower() + _("b").decode("utf-8"))
            specs.append(abbr.lower())
        else:
            specs.append(_lowerASCII(abbr) + "b")
            specs.append(_lowerASCII(abbr))

    return specs

def _parseSpec(spec):
    """ Parse string representation of size. """
    if not spec:
        raise ValueError("invalid size specification", spec)

    # Replace the localized radix character with a .
    radix = locale.nl_langinfo(locale.RADIXCHAR)
    if radix != '.':
        spec = spec.replace(radix, '.')

    # Match the string using only digit/space/not-space, since the
    # string might be non-English and contain non-letter characters
    # that Python doesn't understand as parts of words.
    m = re.match(r'(-?\s*[0-9.]+)\s*([^\s]*)$', spec.strip())
    if not m:
        raise ValueError("invalid size specification", spec)

    try:
        size = Decimal(m.groups()[0])
    except InvalidOperation:
        raise ValueError("invalid size specification", spec)

    specifier = m.groups()[1]

    # Only attempt to parse as English if all characters are ASCII
    try:
        # This will raise UnicodeDecodeError if specifier contains non-ascii
        # characters
        spec_ascii = specifier.decode("ascii")

        # Convert back to a str type to match the _bytes and _prefixes arrays
        spec_ascii = str(spec_ascii)

        # Use the ASCII-only lowercase mapping
        spec_ascii = _lowerASCII(spec_ascii)
    except UnicodeDecodeError:
        pass
    else:
        if spec_ascii and not spec_ascii.endswith("b"):
            spec_ascii += "ib"

        if spec_ascii in _bytes or not spec_ascii:
            return size

        for factor, prefix, abbr in _prefixes:
            check = _makeSpecs(prefix, abbr, False)

            if spec_ascii in check:
                return size * factor

    # No English match found, try localized size specs. Accept any utf-8
    # character and leave the result as a unicode object.
    spec_local = specifier.decode("utf-8")

    # Use the locale-specific lowercasing
    spec_local = spec_local.lower()

    if spec_local in _xlated_bytes():
        return size

    for factor, prefix, abbr in _xlated_prefixes():
        check  = _makeSpecs(prefix, abbr, True)

        if spec_local in check:
            return size * factor

    raise ValueError("invalid size specification", spec)

class Size(Decimal):
    """ Common class to represent storage device and filesystem sizes.
        Can handle parsing strings such as 45MB or 6.7GB to initialize
        itself, or can be initialized with a numerical size in bytes.
        Also generates human readable strings to a specified number of
        decimal places.
    """

    def __new__(cls, bytes=None,  spec=None):
        """ Initialize a new Size object.  Must pass either bytes or spec,
            but not both.  The bytes parameter is a numerical value for the size
            this object represents, in bytes.  The spec parameter is a string
            specification of the size using any of the size specifiers in the
            _decimalPrefixes or _binaryPrefixes lists combined with a 'b' or
            'B'.  For example, to specify 640 kilobytes, you could pass any of
            these parameters:

                spec="640kb"
                spec="640 kb"
                spec="640KB"
                spec="640 KB"
                spec="640 kilobytes"

            If you want to use spec to pass a bytes value, you can use the
            letter 'b' or 'B' or simply leave the specifier off and bytes
            will be assumed.
        """
        if bytes and spec:
            raise SizeParamsError("only specify one parameter")

        if bytes is not None:
            if isinstance(bytes, (int, long, float, Decimal)):
                value = Decimal(bytes)
            elif isinstance(bytes, Size):
                value = Decimal(bytes.convertTo("b"))
            else:
                raise ValueError("invalid value for bytes param")
        elif spec:
            value = _parseSpec(spec)
        else:
            raise SizeParamsError("missing bytes= or spec=")

        # drop any partial byte
        value = value.to_integral_value(rounding=ROUND_DOWN)
        self = Decimal.__new__(cls, value=value)
        return self

    def __str__(self, eng=False, context=None):
        # Convert the result of humanReadable from unicode to str
        return self.humanReadable()

    def __repr__(self):
        return "Size('%s')" % self

    def __deepcopy__(self, memo):
        return Size(bytes=self.convertTo(spec="b"))

    def __add__(self, other, context=None):
        return Size(bytes=Decimal.__add__(self, other, context=context))

    # needed to make sum() work with Size arguments
    def __radd__(self, other, context=None):
        return Size(bytes=Decimal.__radd__(self, other, context=context))

    def __sub__(self, other, context=None):
        # subtraction is implemented using __add__ and negation, so we'll
        # be getting passed a Size
        return Decimal.__sub__(self, other, context=context)

    def __mul__(self, other, context=None):
        return Size(bytes=Decimal.__mul__(self, other, context=context))

    def __div__(self, other, context=None):
        return Size(bytes=Decimal.__div__(self, other, context=context))

    def __mod__(self, other, context=None):
        return Size(bytes=Decimal.__mod__(self, other, context=context))

    def convertTo(self, spec="b"):
        """ Return the size in the units indicated by the specifier.  The
            specifier can be prefixes from the _decimalPrefixes and
            _binaryPrefixes lists combined with 'b' or 'B' for abbreviations) or
            'bytes' (for prefixes like kilo or mega).  The size is returned as a
            Decimal.
        """
        spec = spec.lower()

        if spec in _bytes:
            return Decimal(self)

        for factor, prefix, abbr in _prefixes:
            check = _makeSpecs(prefix, abbr, False)

            if spec in check:
                return Decimal(self / Decimal(factor))

        return None

    def humanReadable(self, places=None, max_places=2):
        """ Return a string representation of this size with appropriate
            size specifier and in the specified number of decimal places
            (i.e. the maximal precision is only achieved by setting both places
            and max_places to None).
        """
        if places is not None and places < 0:
            raise SizePlacesError("places= must be >=0 or None")

        if max_places is not None and max_places < 0:
            raise SizePlacesError("max_places= must be >=0 or None")

        in_bytes = int(Decimal(self))
        if abs(in_bytes) < 1000:
            return "%d %s" % (in_bytes, _("B"))

        prev_prefix = None
        for prefix_item in _xlated_prefixes():
            factor, prefix, abbr = prefix_item
            newcheck = super(Size, self).__div__(Decimal(factor))

            if abs(newcheck) < 1000:
                # nice value, use this factor, prefix and abbr
                break
            prev_prefix = prefix_item
        else:
            # no nice value found, just return size in bytes
            return "%s %s" % (in_bytes, _("B"))

        if abs(newcheck) < 10:
            if prev_prefix:
                factor, prefix, abbr = prev_prefix
                newcheck = super(Size, self).__div__(Decimal(factor))
            else:
                # less than 10 KiB
                return "%s %s" % (in_bytes, _("B"))

        retval = newcheck
        if places is not None:
            retval = round(newcheck, places)

        if max_places is not None:
            if places is not None:
                limit = min((places, max_places))
            else:
                limit = max_places
            retval = round(newcheck, limit)

        # Format the value with '.' as the decimal separator
        # If necessary, substitute with a localized separator before returning
        retval_str = str(retval)
        radix = locale.nl_langinfo(locale.RADIXCHAR)
        if radix != '.':
            retval_str = retval_str.replace('.', radix)

        # abbr and prefix are unicode objects so that lower/upper work correctly
        # Convert them to str before concatenating so that the return type is
        # str.
        # pylint: disable=undefined-loop-variable
        if abbr:
            return retval_str + " " + abbr.encode("utf-8") + _("B")
        else:
            return retval_str + " " + prefix.encode("utf-8") + P_("byte", "bytes", newcheck)
<commit_msg>If size is an integer value, show it as an integer value<commit_after># size.py
# Python module to represent storage sizes
#
# Copyright (C) 2010  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.  You should have received a copy of the
# GNU General Public License along with this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): David Cantrell <dcantrell@redhat.com>

import re
import string
import locale
from collections import namedtuple

from decimal import Decimal
from decimal import InvalidOperation
from decimal import ROUND_DOWN

from .errors import SizeParamsError, SizePlacesError
from .i18n import _, P_, N_

# Container for size unit prefix information
_Prefix = namedtuple("Prefix", ["factor", "prefix", "abbr"])

# Decimal prefixes for different size increments, along with the name
# and accepted abbreviation for the prefix.  These prefixes are all
# for 'bytes'.
_decimalPrefixes = [_Prefix(1000, N_("kilo"), N_("k")),
                    _Prefix(1000**2, N_("mega"), N_("M")),
                    _Prefix(1000**3, N_("giga"), N_("G")),
                    _Prefix(1000**4, N_("tera"), N_("T")),
                    _Prefix(1000**5, N_("peta"), N_("P")),
                    _Prefix(1000**6, N_("exa"), N_("E")),
                    _Prefix(1000**7, N_("zetta"), N_("Z")),
                    _Prefix(1000**8, N_("yotta"), N_("Y"))]

# Binary prefixes for the different size increments.  Same structure
# as the above list.
_binaryPrefixes = [_Prefix(1024, N_("kibi"), N_("Ki")),
                   _Prefix(1024**2, N_("mebi"), N_("Mi")),
                   _Prefix(1024**3, N_("gibi"), N_("Gi")),
                   _Prefix(1024**4, N_("tebi"), N_("Ti")),
                   _Prefix(1024**5, N_("pebi"), N_("Pi")),
                   _Prefix(1024**6, N_("exbi"), N_("Ei")),
                   _Prefix(1024**7, N_("zebi"), N_("Zi")),
                   _Prefix(1024**8, N_("yobi"), N_("Yi"))]

_bytes = [N_('B'), N_('b'), N_('byte'), N_('bytes')]
_prefixes = _binaryPrefixes + _decimalPrefixes

# Translated versions of the byte and prefix arrays
# All strings are decoded as utf-8 so that locale-specific upper/lower functions work
def _xlated_bytes():
    """Return a translated version of the bytes list as a list of unicode strings"""
    return [_(b).decode("utf-8") for b in _bytes]

def _xlated_binary_prefixes():
    return (_Prefix(p.factor, _(p.prefix).decode("utf-8"), _(p.abbr).decode("utf-8")) \
            for p in _binaryPrefixes)

def _xlated_prefixes():
    """Return translated prefixes as unicode strings"""
    xlated_binary = list(_xlated_binary_prefixes())
    xlated_decimal = [_Prefix(p.factor, _(p.prefix).decode("utf-8"), _(p.abbr).decode("utf-8")) \
                      for p in _decimalPrefixes]

    return xlated_binary + xlated_decimal

_ASCIIlower_table = string.maketrans(string.ascii_uppercase, string.ascii_lowercase)
def _lowerASCII(s):
    """Convert a string to lowercase using only ASCII character definitions."""
    return string.translate(s, _ASCIIlower_table)

def _makeSpecs(prefix, abbr, xlate):
    """ Internal method used to generate a list of specifiers. """
    specs = []

    if prefix:
        if xlate:
            specs.append(prefix.lower() + _("byte").decode("utf-8"))
            specs.append(prefix.lower() + _("bytes").decode("utf-8"))
        else:
            specs.append(_lowerASCII(prefix) + "byte")
            specs.append(_lowerASCII(prefix) + "bytes")

    if abbr:
        if xlate:
            specs.append(abbr.lower() + _("b").decode("utf-8"))
            specs.append(abbr.lower())
        else:
            specs.append(_lowerASCII(abbr) + "b")
            specs.append(_lowerASCII(abbr))

    return specs

def _parseSpec(spec):
    """ Parse string representation of size. """
    if not spec:
        raise ValueError("invalid size specification", spec)

    # Replace the localized radix character with a .
    radix = locale.nl_langinfo(locale.RADIXCHAR)
    if radix != '.':
        spec = spec.replace(radix, '.')

    # Match the string using only digit/space/not-space, since the
    # string might be non-English and contain non-letter characters
    # that Python doesn't understand as parts of words.
    m = re.match(r'(-?\s*[0-9.]+)\s*([^\s]*)$', spec.strip())
    if not m:
        raise ValueError("invalid size specification", spec)

    try:
        size = Decimal(m.groups()[0])
    except InvalidOperation:
        raise ValueError("invalid size specification", spec)

    specifier = m.groups()[1]

    # Only attempt to parse as English if all characters are ASCII
    try:
        # This will raise UnicodeDecodeError if specifier contains non-ascii
        # characters
        spec_ascii = specifier.decode("ascii")

        # Convert back to a str type to match the _bytes and _prefixes arrays
        spec_ascii = str(spec_ascii)

        # Use the ASCII-only lowercase mapping
        spec_ascii = _lowerASCII(spec_ascii)
    except UnicodeDecodeError:
        pass
    else:
        if spec_ascii and not spec_ascii.endswith("b"):
            spec_ascii += "ib"

        if spec_ascii in _bytes or not spec_ascii:
            return size

        for factor, prefix, abbr in _prefixes:
            check = _makeSpecs(prefix, abbr, False)

            if spec_ascii in check:
                return size * factor

    # No English match found, try localized size specs. Accept any utf-8
    # character and leave the result as a unicode object.
    spec_local = specifier.decode("utf-8")

    # Use the locale-specific lowercasing
    spec_local = spec_local.lower()

    if spec_local in _xlated_bytes():
        return size

    for factor, prefix, abbr in _xlated_prefixes():
        check  = _makeSpecs(prefix, abbr, True)

        if spec_local in check:
            return size * factor

    raise ValueError("invalid size specification", spec)

class Size(Decimal):
    """ Common class to represent storage device and filesystem sizes.
        Can handle parsing strings such as 45MB or 6.7GB to initialize
        itself, or can be initialized with a numerical size in bytes.
        Also generates human readable strings to a specified number of
        decimal places.
    """

    def __new__(cls, bytes=None,  spec=None):
        """ Initialize a new Size object.  Must pass either bytes or spec,
            but not both.  The bytes parameter is a numerical value for the size
            this object represents, in bytes.  The spec parameter is a string
            specification of the size using any of the size specifiers in the
            _decimalPrefixes or _binaryPrefixes lists combined with a 'b' or
            'B'.  For example, to specify 640 kilobytes, you could pass any of
            these parameters:

                spec="640kb"
                spec="640 kb"
                spec="640KB"
                spec="640 KB"
                spec="640 kilobytes"

            If you want to use spec to pass a bytes value, you can use the
            letter 'b' or 'B' or simply leave the specifier off and bytes
            will be assumed.
        """
        if bytes and spec:
            raise SizeParamsError("only specify one parameter")

        if bytes is not None:
            if isinstance(bytes, (int, long, float, Decimal)):
                value = Decimal(bytes)
            elif isinstance(bytes, Size):
                value = Decimal(bytes.convertTo("b"))
            else:
                raise ValueError("invalid value for bytes param")
        elif spec:
            value = _parseSpec(spec)
        else:
            raise SizeParamsError("missing bytes= or spec=")

        # drop any partial byte
        value = value.to_integral_value(rounding=ROUND_DOWN)
        self = Decimal.__new__(cls, value=value)
        return self

    def __str__(self, eng=False, context=None):
        # Convert the result of humanReadable from unicode to str
        return self.humanReadable()

    def __repr__(self):
        return "Size('%s')" % self

    def __deepcopy__(self, memo):
        return Size(bytes=self.convertTo(spec="b"))

    def __add__(self, other, context=None):
        return Size(bytes=Decimal.__add__(self, other, context=context))

    # needed to make sum() work with Size arguments
    def __radd__(self, other, context=None):
        return Size(bytes=Decimal.__radd__(self, other, context=context))

    def __sub__(self, other, context=None):
        # subtraction is implemented using __add__ and negation, so we'll
        # be getting passed a Size
        return Decimal.__sub__(self, other, context=context)

    def __mul__(self, other, context=None):
        return Size(bytes=Decimal.__mul__(self, other, context=context))

    def __div__(self, other, context=None):
        return Size(bytes=Decimal.__div__(self, other, context=context))

    def __mod__(self, other, context=None):
        return Size(bytes=Decimal.__mod__(self, other, context=context))

    def convertTo(self, spec="b"):
        """ Return the size in the units indicated by the specifier.  The
            specifier can be prefixes from the _decimalPrefixes and
            _binaryPrefixes lists combined with 'b' or 'B' for abbreviations) or
            'bytes' (for prefixes like kilo or mega).  The size is returned as a
            Decimal.
        """
        spec = spec.lower()

        if spec in _bytes:
            return Decimal(self)

        for factor, prefix, abbr in _prefixes:
            check = _makeSpecs(prefix, abbr, False)

            if spec in check:
                return Decimal(self / Decimal(factor))

        return None

    def humanReadable(self, places=None, max_places=2):
        """ Return a string representation of this size with appropriate
            size specifier and in the specified number of decimal places
            (i.e. the maximal precision is only achieved by setting both places
            and max_places to None).
        """
        if places is not None and places < 0:
            raise SizePlacesError("places= must be >=0 or None")

        if max_places is not None and max_places < 0:
            raise SizePlacesError("max_places= must be >=0 or None")

        in_bytes = int(Decimal(self))
        if abs(in_bytes) < 1000:
            return "%d %s" % (in_bytes, _("B"))

        prev_prefix = None
        for prefix_item in _xlated_prefixes():
            factor, prefix, abbr = prefix_item
            newcheck = super(Size, self).__div__(Decimal(factor))

            if abs(newcheck) < 1000:
                # nice value, use this factor, prefix and abbr
                break
            prev_prefix = prefix_item
        else:
            # no nice value found, just return size in bytes
            return "%s %s" % (in_bytes, _("B"))

        if abs(newcheck) < 10:
            if prev_prefix:
                factor, prefix, abbr = prev_prefix
                newcheck = super(Size, self).__div__(Decimal(factor))
            else:
                # less than 10 KiB
                return "%s %s" % (in_bytes, _("B"))

        retval = newcheck
        if places is not None:
            retval = round(newcheck, places)

        if max_places is not None:
            if places is not None:
                limit = min((places, max_places))
            else:
                limit = max_places
            retval = round(newcheck, limit)

        if retval == int(retval):
            # integer value, no point in showing ".0" at the end
            retval = int(retval)

        # Format the value with '.' as the decimal separator
        # If necessary, substitute with a localized separator before returning
        retval_str = str(retval)
        radix = locale.nl_langinfo(locale.RADIXCHAR)
        if radix != '.':
            retval_str = retval_str.replace('.', radix)

        # abbr and prefix are unicode objects so that lower/upper work correctly
        # Convert them to str before concatenating so that the return type is
        # str.
        # pylint: disable=undefined-loop-variable
        if abbr:
            return retval_str + " " + abbr.encode("utf-8") + _("B")
        else:
            return retval_str + " " + prefix.encode("utf-8") + P_("byte", "bytes", newcheck)
<|endoftext|>