humanfriendly-4.18/0000755000175000017500000000000013433604174014600 5ustar peterpeter00000000000000humanfriendly-4.18/requirements-tests.txt0000644000175000017500000000014213362535475021231 0ustar peterpeter00000000000000# Test suite requirements. capturer >= 2.1 coloredlogs >= 2.0 pytest >= 3.0.7 pytest-cov >= 2.4.0 humanfriendly-4.18/MANIFEST.in0000664000175000017500000000004713142152246016334 0ustar peterpeter00000000000000include *.rst include *.txt graft docs humanfriendly-4.18/setup.cfg0000664000175000017500000000007513433604174016425 0ustar peterpeter00000000000000[wheel] universal = 1 [egg_info] tag_build = tag_date = 0 humanfriendly-4.18/README.rst0000644000175000017500000001456313362535475016310 0ustar peterpeter00000000000000humanfriendly: Human friendly input/output in Python ==================================================== .. image:: https://travis-ci.org/xolox/python-humanfriendly.svg?branch=master :target: https://travis-ci.org/xolox/python-humanfriendly .. image:: https://coveralls.io/repos/xolox/python-humanfriendly/badge.png?branch=master :target: https://coveralls.io/r/xolox/python-humanfriendly?branch=master The functions and classes in the `humanfriendly` package can be used to make text interfaces more user friendly. Some example features: - Parsing and formatting numbers, file sizes, pathnames and timespans in simple, human friendly formats. - Easy to use timers for long running operations, with human friendly formatting of the resulting timespans. - Prompting the user to select a choice from a list of options by typing the option's number or a unique substring of the option. - Terminal interaction including text styling (ANSI escape sequences), user friendly rendering of usage messages and querying the terminal for its size. The `humanfriendly` package is currently tested on Python 2.6, 2.7, 3.4, 3.5, 3.6, 3.7 and PyPy (2.7) on Linux and Mac OS X. While the intention is to support Windows as well, you may encounter some rough edges. .. contents:: :local: Getting started --------------- It's very simple to start using the `humanfriendly` package:: >>> import humanfriendly >>> user_input = raw_input("Enter a readable file size: ") Enter a readable file size: 16G >>> num_bytes = humanfriendly.parse_size(user_input) >>> print num_bytes 16000000000 >>> print "You entered:", humanfriendly.format_size(num_bytes) You entered: 16 GB >>> print "You entered:", humanfriendly.format_size(num_bytes, binary=True) You entered: 14.9 GiB Command line ------------ .. A DRY solution to avoid duplication of the `humanfriendly --help' text: .. .. [[[cog .. from humanfriendly.usage import inject_usage .. inject_usage('humanfriendly.cli') .. ]]] **Usage:** `humanfriendly [OPTIONS]` Human friendly input/output (text formatting) on the command line based on the Python package with the same name. **Supported options:** .. csv-table:: :header: Option, Description :widths: 30, 70 "``-c``, ``--run-command``","Execute an external command (given as the positional arguments) and render a spinner and timer while the command is running. The exit status of the command is propagated." ``--format-table``,"Read tabular data from standard input (each line is a row and each whitespace separated field is a column), format the data as a table and print the resulting table to standard output. See also the ``--delimiter`` option." "``-d``, ``--delimiter=VALUE``","Change the delimiter used by ``--format-table`` to ``VALUE`` (a string). By default all whitespace is treated as a delimiter." "``-l``, ``--format-length=LENGTH``","Convert a length count (given as the integer or float ``LENGTH``) into a human readable string and print that string to standard output." "``-n``, ``--format-number=VALUE``","Format a number (given as the integer or floating point number ``VALUE``) with thousands separators and two decimal places (if needed) and print the formatted number to standard output." "``-s``, ``--format-size=BYTES``","Convert a byte count (given as the integer ``BYTES``) into a human readable string and print that string to standard output." "``-b``, ``--binary``","Change the output of ``-s``, ``--format-size`` to use binary multiples of bytes (base-2) instead of the default decimal multiples of bytes (base-10)." "``-t``, ``--format-timespan=SECONDS``","Convert a number of seconds (given as the floating point number ``SECONDS``) into a human readable timespan and print that string to standard output." ``--parse-length=VALUE``,"Parse a human readable length (given as the string ``VALUE``) and print the number of metres to standard output." ``--parse-size=VALUE``,"Parse a human readable data size (given as the string ``VALUE``) and print the number of bytes to standard output." ``--demo``,"Demonstrate changing the style and color of the terminal font using ANSI escape sequences." "``-h``, ``--help``",Show this message and exit. .. [[[end]]] A note about size units ----------------------- When I originally published the `humanfriendly` package I went with binary multiples of bytes (powers of two). It was pointed out several times that this was a poor choice (see issue `#4`_ and pull requests `#8`_ and `#9`_) and thus the new default became decimal multiples of bytes (powers of ten): +------+---------------+---------------+ | Unit | Binary value | Decimal value | +------+---------------+---------------+ | KB | 1024 | 1000 + +------+---------------+---------------+ | MB | 1048576 | 1000000 | +------+---------------+---------------+ | GB | 1073741824 | 1000000000 | +------+---------------+---------------+ | TB | 1099511627776 | 1000000000000 | +------+---------------+---------------+ | etc | | | +------+---------------+---------------+ The option to use binary multiples of bytes remains by passing the keyword argument `binary=True` to the `format_size()`_ and `parse_size()`_ functions. Contact ------- The latest version of `humanfriendly` is available on PyPI_ and GitHub_. The documentation is hosted on `Read the Docs`_ and includes a changelog_. For bug reports please create an issue on GitHub_. If you have questions, suggestions, etc. feel free to send me an e-mail at `peter@peterodding.com`_. License ------- This software is licensed under the `MIT license`_. © 2018 Peter Odding. .. External references: .. _#4: https://github.com/xolox/python-humanfriendly/issues/4 .. _#8: https://github.com/xolox/python-humanfriendly/pull/8 .. _#9: https://github.com/xolox/python-humanfriendly/pull/9 .. _changelog: https://humanfriendly.readthedocs.io/en/latest/changelog.html .. _format_size(): https://humanfriendly.readthedocs.io/en/latest/#humanfriendly.format_size .. _GitHub: https://github.com/xolox/python-humanfriendly .. _MIT license: http://en.wikipedia.org/wiki/MIT_License .. _parse_size(): https://humanfriendly.readthedocs.io/en/latest/#humanfriendly.parse_size .. _peter@peterodding.com: peter@peterodding.com .. _PyPI: https://pypi.python.org/pypi/humanfriendly .. _Read the Docs: https://humanfriendly.readthedocs.io humanfriendly-4.18/PKG-INFO0000644000175000017500000002240613433604174015701 0ustar peterpeter00000000000000Metadata-Version: 1.1 Name: humanfriendly Version: 4.18 Summary: Human friendly output for text interfaces using Python Home-page: https://humanfriendly.readthedocs.io Author: Peter Odding Author-email: peter@peterodding.com License: MIT Description: humanfriendly: Human friendly input/output in Python ==================================================== .. image:: https://travis-ci.org/xolox/python-humanfriendly.svg?branch=master :target: https://travis-ci.org/xolox/python-humanfriendly .. image:: https://coveralls.io/repos/xolox/python-humanfriendly/badge.png?branch=master :target: https://coveralls.io/r/xolox/python-humanfriendly?branch=master The functions and classes in the `humanfriendly` package can be used to make text interfaces more user friendly. Some example features: - Parsing and formatting numbers, file sizes, pathnames and timespans in simple, human friendly formats. - Easy to use timers for long running operations, with human friendly formatting of the resulting timespans. - Prompting the user to select a choice from a list of options by typing the option's number or a unique substring of the option. - Terminal interaction including text styling (ANSI escape sequences), user friendly rendering of usage messages and querying the terminal for its size. The `humanfriendly` package is currently tested on Python 2.6, 2.7, 3.4, 3.5, 3.6, 3.7 and PyPy (2.7) on Linux and Mac OS X. While the intention is to support Windows as well, you may encounter some rough edges. .. contents:: :local: Getting started --------------- It's very simple to start using the `humanfriendly` package:: >>> import humanfriendly >>> user_input = raw_input("Enter a readable file size: ") Enter a readable file size: 16G >>> num_bytes = humanfriendly.parse_size(user_input) >>> print num_bytes 16000000000 >>> print "You entered:", humanfriendly.format_size(num_bytes) You entered: 16 GB >>> print "You entered:", humanfriendly.format_size(num_bytes, binary=True) You entered: 14.9 GiB Command line ------------ .. A DRY solution to avoid duplication of the `humanfriendly --help' text: .. .. [[[cog .. from humanfriendly.usage import inject_usage .. inject_usage('humanfriendly.cli') .. ]]] **Usage:** `humanfriendly [OPTIONS]` Human friendly input/output (text formatting) on the command line based on the Python package with the same name. **Supported options:** .. csv-table:: :header: Option, Description :widths: 30, 70 "``-c``, ``--run-command``","Execute an external command (given as the positional arguments) and render a spinner and timer while the command is running. The exit status of the command is propagated." ``--format-table``,"Read tabular data from standard input (each line is a row and each whitespace separated field is a column), format the data as a table and print the resulting table to standard output. See also the ``--delimiter`` option." "``-d``, ``--delimiter=VALUE``","Change the delimiter used by ``--format-table`` to ``VALUE`` (a string). By default all whitespace is treated as a delimiter." "``-l``, ``--format-length=LENGTH``","Convert a length count (given as the integer or float ``LENGTH``) into a human readable string and print that string to standard output." "``-n``, ``--format-number=VALUE``","Format a number (given as the integer or floating point number ``VALUE``) with thousands separators and two decimal places (if needed) and print the formatted number to standard output." "``-s``, ``--format-size=BYTES``","Convert a byte count (given as the integer ``BYTES``) into a human readable string and print that string to standard output." "``-b``, ``--binary``","Change the output of ``-s``, ``--format-size`` to use binary multiples of bytes (base-2) instead of the default decimal multiples of bytes (base-10)." "``-t``, ``--format-timespan=SECONDS``","Convert a number of seconds (given as the floating point number ``SECONDS``) into a human readable timespan and print that string to standard output." ``--parse-length=VALUE``,"Parse a human readable length (given as the string ``VALUE``) and print the number of metres to standard output." ``--parse-size=VALUE``,"Parse a human readable data size (given as the string ``VALUE``) and print the number of bytes to standard output." ``--demo``,"Demonstrate changing the style and color of the terminal font using ANSI escape sequences." "``-h``, ``--help``",Show this message and exit. .. [[[end]]] A note about size units ----------------------- When I originally published the `humanfriendly` package I went with binary multiples of bytes (powers of two). It was pointed out several times that this was a poor choice (see issue `#4`_ and pull requests `#8`_ and `#9`_) and thus the new default became decimal multiples of bytes (powers of ten): +------+---------------+---------------+ | Unit | Binary value | Decimal value | +------+---------------+---------------+ | KB | 1024 | 1000 + +------+---------------+---------------+ | MB | 1048576 | 1000000 | +------+---------------+---------------+ | GB | 1073741824 | 1000000000 | +------+---------------+---------------+ | TB | 1099511627776 | 1000000000000 | +------+---------------+---------------+ | etc | | | +------+---------------+---------------+ The option to use binary multiples of bytes remains by passing the keyword argument `binary=True` to the `format_size()`_ and `parse_size()`_ functions. Contact ------- The latest version of `humanfriendly` is available on PyPI_ and GitHub_. The documentation is hosted on `Read the Docs`_ and includes a changelog_. For bug reports please create an issue on GitHub_. If you have questions, suggestions, etc. feel free to send me an e-mail at `peter@peterodding.com`_. License ------- This software is licensed under the `MIT license`_. © 2018 Peter Odding. .. External references: .. _#4: https://github.com/xolox/python-humanfriendly/issues/4 .. _#8: https://github.com/xolox/python-humanfriendly/pull/8 .. _#9: https://github.com/xolox/python-humanfriendly/pull/9 .. _changelog: https://humanfriendly.readthedocs.io/en/latest/changelog.html .. _format_size(): https://humanfriendly.readthedocs.io/en/latest/#humanfriendly.format_size .. _GitHub: https://github.com/xolox/python-humanfriendly .. _MIT license: http://en.wikipedia.org/wiki/MIT_License .. _parse_size(): https://humanfriendly.readthedocs.io/en/latest/#humanfriendly.parse_size .. _peter@peterodding.com: peter@peterodding.com .. _PyPI: https://pypi.python.org/pypi/humanfriendly .. _Read the Docs: https://humanfriendly.readthedocs.io Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Environment :: Console Classifier: Framework :: Sphinx :: Extension Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Communications Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: System :: Shells Classifier: Topic :: System :: System Shells Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Terminals Classifier: Topic :: Text Processing :: General Classifier: Topic :: Text Processing :: Linguistic Classifier: Topic :: Utilities humanfriendly-4.18/requirements-travis.txt0000644000175000017500000000012513362535475021400 0ustar peterpeter00000000000000--requirement=requirements-checks.txt --requirement=requirements-tests.txt coveralls humanfriendly-4.18/humanfriendly/0000755000175000017500000000000013433604174017445 5ustar peterpeter00000000000000humanfriendly-4.18/humanfriendly/text.py0000644000175000017500000003530613433604150021004 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: February 21, 2019 # URL: https://humanfriendly.readthedocs.io """ Simple text manipulation functions. The :mod:`~humanfriendly.text` module contains simple functions to manipulate text: - The :func:`concatenate()` and :func:`pluralize()` functions make it easy to generate human friendly output. - The :func:`format()`, :func:`compact()` and :func:`dedent()` functions provide a clean and simple to use syntax for composing large text fragments with interpolated variables. - The :func:`tokenize()` function parses simple user input. """ # Standard library modules. import math import numbers import random import re import string import textwrap # Public identifiers that require documentation. __all__ = ( 'compact', 'compact_empty_lines', 'concatenate', 'dedent', 'format', 'is_empty_line', 'join_lines', 'pluralize', 'random_string', 'split', 'split_paragraphs', 'tokenize', 'trim_empty_lines', ) def compact(text, *args, **kw): ''' Compact whitespace in a string. Trims leading and trailing whitespace, replaces runs of whitespace characters with a single space and interpolates any arguments using :func:`format()`. :param text: The text to compact (a string). :param args: Any positional arguments are interpolated using :func:`format()`. :param kw: Any keyword arguments are interpolated using :func:`format()`. :returns: The compacted text (a string). Here's an example of how I like to use the :func:`compact()` function, this is an example from a random unrelated project I'm working on at the moment:: raise PortDiscoveryError(compact(""" Failed to discover port(s) that Apache is listening on! Maybe I'm parsing the wrong configuration file? ({filename}) """, filename=self.ports_config)) The combination of :func:`compact()` and Python's multi line strings allows me to write long text fragments with interpolated variables that are easy to write, easy to read and work well with Python's whitespace sensitivity. ''' non_whitespace_tokens = text.split() compacted_text = ' '.join(non_whitespace_tokens) return format(compacted_text, *args, **kw) def compact_empty_lines(text): """ Replace repeating empty lines with a single empty line (similar to ``cat -s``). :param text: The text in which to compact empty lines (a string). :returns: The text with empty lines compacted (a string). """ i = 0 lines = text.splitlines(True) while i < len(lines): if i > 0 and is_empty_line(lines[i - 1]) and is_empty_line(lines[i]): lines.pop(i) else: i += 1 return ''.join(lines) def concatenate(items): """ Concatenate a list of items in a human friendly way. :param items: A sequence of strings. :returns: A single string. >>> from humanfriendly.text import concatenate >>> concatenate(["eggs", "milk", "bread"]) 'eggs, milk and bread' """ items = list(items) if len(items) > 1: return ', '.join(items[:-1]) + ' and ' + items[-1] elif items: return items[0] else: return '' def dedent(text, *args, **kw): """ Dedent a string (remove common leading whitespace from all lines). Removes common leading whitespace from all lines in the string using :func:`textwrap.dedent()`, removes leading and trailing empty lines using :func:`trim_empty_lines()` and interpolates any arguments using :func:`format()`. :param text: The text to dedent (a string). :param args: Any positional arguments are interpolated using :func:`format()`. :param kw: Any keyword arguments are interpolated using :func:`format()`. :returns: The dedented text (a string). The :func:`compact()` function's documentation contains an example of how I like to use the :func:`compact()` and :func:`dedent()` functions. The main difference is that I use :func:`compact()` for text that will be presented to the user (where whitespace is not so significant) and :func:`dedent()` for data file and code generation tasks (where newlines and indentation are very significant). """ dedented_text = textwrap.dedent(text) trimmed_text = trim_empty_lines(dedented_text) return format(trimmed_text, *args, **kw) def format(text, *args, **kw): """ Format a string using the string formatting operator and/or :func:`str.format()`. :param text: The text to format (a string). :param args: Any positional arguments are interpolated into the text using the string formatting operator (``%``). If no positional arguments are given no interpolation is done. :param kw: Any keyword arguments are interpolated into the text using the :func:`str.format()` function. If no keyword arguments are given no interpolation is done. :returns: The text with any positional and/or keyword arguments interpolated (a string). The implementation of this function is so trivial that it seems silly to even bother writing and documenting it. Justifying this requires some context :-). **Why format() instead of the string formatting operator?** For really simple string interpolation Python's string formatting operator is ideal, but it does have some strange quirks: - When you switch from interpolating a single value to interpolating multiple values you have to wrap them in tuple syntax. Because :func:`format()` takes a `variable number of arguments`_ it always receives a tuple (which saves me a context switch :-). Here's an example: >>> from humanfriendly.text import format >>> # The string formatting operator. >>> print('the magic number is %s' % 42) the magic number is 42 >>> print('the magic numbers are %s and %s' % (12, 42)) the magic numbers are 12 and 42 >>> # The format() function. >>> print(format('the magic number is %s', 42)) the magic number is 42 >>> print(format('the magic numbers are %s and %s', 12, 42)) the magic numbers are 12 and 42 - When you interpolate a single value and someone accidentally passes in a tuple your code raises a :exc:`~exceptions.TypeError`. Because :func:`format()` takes a `variable number of arguments`_ it always receives a tuple so this can never happen. Here's an example: >>> # How expecting to interpolate a single value can fail. >>> value = (12, 42) >>> print('the magic value is %s' % value) Traceback (most recent call last): File "", line 1, in TypeError: not all arguments converted during string formatting >>> # The following line works as intended, no surprises here! >>> print(format('the magic value is %s', value)) the magic value is (12, 42) **Why format() instead of the str.format() method?** When you're doing complex string interpolation the :func:`str.format()` function results in more readable code, however I frequently find myself adding parentheses to force evaluation order. The :func:`format()` function avoids this because of the relative priority between the comma and dot operators. Here's an example: >>> "{adjective} example" + " " + "(can't think of anything less {adjective})".format(adjective='silly') "{adjective} example (can't think of anything less silly)" >>> ("{adjective} example" + " " + "(can't think of anything less {adjective})").format(adjective='silly') "silly example (can't think of anything less silly)" >>> format("{adjective} example" + " " + "(can't think of anything less {adjective})", adjective='silly') "silly example (can't think of anything less silly)" The :func:`compact()` and :func:`dedent()` functions are wrappers that combine :func:`format()` with whitespace manipulation to make it easy to write nice to read Python code. .. _variable number of arguments: https://docs.python.org/2/tutorial/controlflow.html#arbitrary-argument-lists """ if args: text %= args if kw: text = text.format(**kw) return text def generate_slug(text, delimiter="-"): """ Convert text to a normalized "slug" without whitespace. :param text: The original text, for example ``Some Random Text!``. :param delimiter: The delimiter used to separate words (defaults to the ``-`` character). :returns: The slug text, for example ``some-random-text``. :raises: :exc:`~exceptions.ValueError` when the provided text is nonempty but results in an empty slug. """ slug = text.lower() escaped = delimiter.replace("\\", "\\\\") slug = re.sub("[^a-z0-9]+", escaped, slug) slug = slug.strip(delimiter) if text and not slug: msg = "The provided text %r results in an empty slug!" raise ValueError(format(msg, text)) return slug def is_empty_line(text): """ Check if a text is empty or contains only whitespace. :param text: The text to check for "emptiness" (a string). :returns: :data:`True` if the text is empty or contains only whitespace, :data:`False` otherwise. """ return len(text) == 0 or text.isspace() def join_lines(text): """ Remove "hard wrapping" from the paragraphs in a string. :param text: The text to reformat (a string). :returns: The text without hard wrapping (a string). This function works by removing line breaks when the last character before a line break and the first character after the line break are both non-whitespace characters. This means that common leading indentation will break :func:`join_lines()` (in that case you can use :func:`dedent()` before calling :func:`join_lines()`). """ return re.sub(r'(\S)\n(\S)', r'\1 \2', text) def pluralize(count, singular, plural=None): """ Combine a count with the singular or plural form of a word. If the plural form of the word is not provided it is obtained by concatenating the singular form of the word with the letter "s". Of course this will not always be correct, which is why you have the option to specify both forms. :param count: The count (a number). :param singular: The singular form of the word (a string). :param plural: The plural form of the word (a string or :data:`None`). :returns: The count and singular/plural word concatenated (a string). """ if not plural: plural = singular + 's' return '%s %s' % (count, singular if math.floor(float(count)) == 1 else plural) def random_string(length=(25, 100), characters=string.ascii_letters): """random_string(length=(25, 100), characters=string.ascii_letters) Generate a random string. :param length: The length of the string to be generated (a number or a tuple with two numbers). If this is a tuple then a random number between the two numbers given in the tuple is used. :param characters: The characters to be used (a string, defaults to :data:`string.ascii_letters`). :returns: A random string. The :func:`random_string()` function is very useful in test suites; by the time I included it in :mod:`humanfriendly.text` I had already included variants of this function in seven different test suites :-). """ if not isinstance(length, numbers.Number): length = random.randint(length[0], length[1]) return ''.join(random.choice(characters) for _ in range(length)) def split(text, delimiter=','): """ Split a comma-separated list of strings. :param text: The text to split (a string). :param delimiter: The delimiter to split on (a string). :returns: A list of zero or more nonempty strings. Here's the default behavior of Python's built in :func:`str.split()` function: >>> 'foo,bar, baz,'.split(',') ['foo', 'bar', ' baz', ''] In contrast here's the default behavior of the :func:`split()` function: >>> from humanfriendly.text import split >>> split('foo,bar, baz,') ['foo', 'bar', 'baz'] Here is an example that parses a nested data structure (a mapping of logging level names to one or more styles per level) that's encoded in a string so it can be set as an environment variable: >>> from pprint import pprint >>> encoded_data = 'debug=green;warning=yellow;error=red;critical=red,bold' >>> parsed_data = dict((k, split(v, ',')) for k, v in (split(kv, '=') for kv in split(encoded_data, ';'))) >>> pprint(parsed_data) {'debug': ['green'], 'warning': ['yellow'], 'error': ['red'], 'critical': ['red', 'bold']} """ return [token.strip() for token in text.split(delimiter) if token and not token.isspace()] def split_paragraphs(text): """ Split a string into paragraphs (one or more lines delimited by an empty line). :param text: The text to split into paragraphs (a string). :returns: A list of strings. """ paragraphs = [] for chunk in text.split('\n\n'): chunk = trim_empty_lines(chunk) if chunk and not chunk.isspace(): paragraphs.append(chunk) return paragraphs def tokenize(text): """ Tokenize a text into numbers and strings. :param text: The text to tokenize (a string). :returns: A list of strings and/or numbers. This function is used to implement robust tokenization of user input in functions like :func:`.parse_size()` and :func:`.parse_timespan()`. It automatically coerces integer and floating point numbers, ignores whitespace and knows how to separate numbers from strings even without whitespace. Some examples to make this more concrete: >>> from humanfriendly.text import tokenize >>> tokenize('42') [42] >>> tokenize('42MB') [42, 'MB'] >>> tokenize('42.5MB') [42.5, 'MB'] >>> tokenize('42.5 MB') [42.5, 'MB'] """ tokenized_input = [] for token in re.split(r'(\d+(?:\.\d+)?)', text): token = token.strip() if re.match(r'\d+\.\d+', token): tokenized_input.append(float(token)) elif token.isdigit(): tokenized_input.append(int(token)) elif token: tokenized_input.append(token) return tokenized_input def trim_empty_lines(text): """ Trim leading and trailing empty lines from the given text. :param text: The text to trim (a string). :returns: The trimmed text (a string). """ lines = text.splitlines(True) while lines and is_empty_line(lines[0]): lines.pop(0) while lines and is_empty_line(lines[-1]): lines.pop(-1) return ''.join(lines) humanfriendly-4.18/humanfriendly/usage.py0000664000175000017500000003270213125024327021123 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: June 24, 2017 # URL: https://humanfriendly.readthedocs.io """ Parsing and reformatting of usage messages. The :mod:`~humanfriendly.usage` module parses and reformats usage messages: - The :func:`format_usage()` function takes a usage message and inserts ANSI escape sequences that highlight items of special significance like command line options, meta variables, etc. The resulting usage message is (intended to be) easier to read on a terminal. - The :func:`render_usage()` function takes a usage message and rewrites it to reStructuredText_ suitable for inclusion in the documentation of a Python package. This provides a DRY solution to keeping a single authoritative definition of the usage message while making it easily available in documentation. As a cherry on the cake it's not just a pre-formatted dump of the usage message but a nicely formatted reStructuredText_ fragment. - The remaining functions in this module support the two functions above. Usage messages in general are free format of course, however the functions in this module assume a certain structure from usage messages in order to successfully parse and reformat them, refer to :func:`parse_usage()` for details. .. _DRY: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText """ # Standard library modules. import csv import functools import logging import re # Standard library module or external dependency (see setup.py). from importlib import import_module # Modules included in our package. from humanfriendly.compat import StringIO from humanfriendly.text import dedent, split_paragraphs, trim_empty_lines # Public identifiers that require documentation. __all__ = ( 'find_meta_variables', 'format_usage', 'import_module', # previously exported (backwards compatibility) 'inject_usage', 'parse_usage', 'render_usage', 'USAGE_MARKER', ) USAGE_MARKER = "Usage:" """The string that starts the first line of a usage message.""" START_OF_OPTIONS_MARKER = "Supported options:" """The string that marks the start of the documented command line options.""" # Compiled regular expression used to tokenize usage messages. USAGE_PATTERN = re.compile(r''' # Make sure whatever we're matching isn't preceded by a non-whitespace # character. (? # Last Change: February 21, 2019 # URL: https://humanfriendly.readthedocs.io """Test suite for the `humanfriendly` package.""" # Standard library modules. import datetime import math import os import random import re import subprocess import sys import time # Modules included in our package. import humanfriendly from humanfriendly import prompts from humanfriendly import coerce_pattern, compact, dedent, trim_empty_lines from humanfriendly.cli import main from humanfriendly.compat import StringIO from humanfriendly.decorators import cached from humanfriendly.prompts import ( TooManyInvalidReplies, prompt_for_confirmation, prompt_for_choice, prompt_for_input, ) from humanfriendly.sphinx import ( setup, special_methods_callback, usage_message_callback, ) from humanfriendly.tables import ( format_pretty_table, format_robust_table, format_rst_table, format_smart_table, ) from humanfriendly.terminal import ( ANSI_CSI, ANSI_RESET, ANSI_SGR, ansi_strip, ansi_style, ansi_width, ansi_wrap, clean_terminal_output, connected_to_terminal, find_terminal_size, get_pager_command, html_to_ansi, message, output, show_pager, terminal_supports_colors, warning, ) from humanfriendly.testing import ( CallableTimedOut, MockedProgram, PatchedAttribute, PatchedItem, TemporaryDirectory, TestCase, retry, run_cli, touch, ) from humanfriendly.text import compact_empty_lines, generate_slug, random_string from humanfriendly.usage import ( find_meta_variables, format_usage, parse_usage, render_usage, ) # Test dependencies. from capturer import CaptureOutput class HumanFriendlyTestCase(TestCase): """Container for the `humanfriendly` test suite.""" exceptionsToSkip = [NotImplementedError] """Translate NotImplementedError into skipped tests.""" def test_skipping(self): """Make sure custom exception types can be skipped.""" raise NotImplementedError() def test_assert_raises(self): """Test :func:`~humanfriendly.testing.TestCase.assertRaises()`.""" e = self.assertRaises(ValueError, humanfriendly.coerce_boolean, 'not a boolean') assert isinstance(e, ValueError) def test_retry_raise(self): """Test :func:`~humanfriendly.testing.retry()` based on assertion errors.""" # Define a helper function that will raise an assertion error on the # first call and return a string on the second call. def success_helper(): if not hasattr(success_helper, 'was_called'): setattr(success_helper, 'was_called', True) assert False else: return 'yes' assert retry(success_helper) == 'yes' # Define a helper function that always raises an assertion error. def failure_helper(): assert False self.assertRaises(AssertionError, retry, failure_helper, timeout=1) def test_retry_return(self): """Test :func:`~humanfriendly.testing.retry()` based on return values.""" # Define a helper function that will return False on the first call and # return a number on the second call. def success_helper(): if not hasattr(success_helper, 'was_called'): # On the first call we return False. setattr(success_helper, 'was_called', True) return False else: # On the second call we return a number. return 42 assert retry(success_helper) == 42 self.assertRaises(CallableTimedOut, retry, lambda: False, timeout=1) def test_mocked_program(self): """Test :class:`humanfriendly.testing.MockedProgram`.""" name = random_string() with MockedProgram(name=name, returncode=42) as directory: assert os.path.isdir(directory) assert os.path.isfile(os.path.join(directory, name)) assert subprocess.call(name) == 42 def test_temporary_directory(self): """Test :class:`humanfriendly.testing.TemporaryDirectory`.""" with TemporaryDirectory() as directory: assert os.path.isdir(directory) temporary_file = os.path.join(directory, 'some-file') with open(temporary_file, 'w') as handle: handle.write("Hello world!") assert not os.path.exists(temporary_file) assert not os.path.exists(directory) def test_touch(self): """Test :func:`humanfriendly.testing.touch()`.""" with TemporaryDirectory() as directory: # Create a file in the temporary directory. filename = os.path.join(directory, random_string()) assert not os.path.isfile(filename) touch(filename) assert os.path.isfile(filename) # Create a file in a subdirectory. filename = os.path.join(directory, random_string(), random_string()) assert not os.path.isfile(filename) touch(filename) assert os.path.isfile(filename) def test_patch_attribute(self): """Test :class:`humanfriendly.testing.PatchedAttribute`.""" class Subject(object): my_attribute = 42 instance = Subject() assert instance.my_attribute == 42 with PatchedAttribute(instance, 'my_attribute', 13) as return_value: assert return_value is instance assert instance.my_attribute == 13 assert instance.my_attribute == 42 def test_patch_item(self): """Test :class:`humanfriendly.testing.PatchedItem`.""" instance = dict(my_item=True) assert instance['my_item'] is True with PatchedItem(instance, 'my_item', False) as return_value: assert return_value is instance assert instance['my_item'] is False assert instance['my_item'] is True def test_run_cli_intercepts_exit(self): """Test that run_cli() intercepts SystemExit.""" returncode, output = run_cli(lambda: sys.exit(42)) self.assertEqual(returncode, 42) def test_run_cli_intercepts_error(self): """Test that run_cli() intercepts exceptions.""" returncode, output = run_cli(self.run_cli_raise_other) self.assertEqual(returncode, 1) def run_cli_raise_other(self): """run_cli() sample that raises an exception.""" raise ValueError() def test_run_cli_intercepts_output(self): """Test that run_cli() intercepts output.""" expected_output = random_string() + "\n" returncode, output = run_cli(lambda: sys.stdout.write(expected_output)) self.assertEqual(returncode, 0) self.assertEqual(output, expected_output) def test_caching_decorator(self): """Test the caching decorator.""" # Confirm that the caching decorator works. a = cached(lambda: random.random()) b = cached(lambda: random.random()) assert a() == a() assert b() == b() # Confirm that functions have their own cache. assert a() != b() def test_compact(self): """Test :func:`humanfriendly.text.compact()`.""" assert compact(' a \n\n b ') == 'a b' assert compact(''' %s template notation ''', 'Simple') == 'Simple template notation' assert compact(''' More {type} template notation ''', type='readable') == 'More readable template notation' def test_compact_empty_lines(self): """Test :func:`humanfriendly.text.compact_empty_lines()`.""" # Simple strings pass through untouched. assert compact_empty_lines('foo') == 'foo' # Horizontal whitespace remains untouched. assert compact_empty_lines('\tfoo') == '\tfoo' # Line breaks should be preserved. assert compact_empty_lines('foo\nbar') == 'foo\nbar' # Vertical whitespace should be preserved. assert compact_empty_lines('foo\n\nbar') == 'foo\n\nbar' # Vertical whitespace should be compressed. assert compact_empty_lines('foo\n\n\nbar') == 'foo\n\nbar' assert compact_empty_lines('foo\n\n\n\nbar') == 'foo\n\nbar' assert compact_empty_lines('foo\n\n\n\n\nbar') == 'foo\n\nbar' def test_dedent(self): """Test :func:`humanfriendly.text.dedent()`.""" assert dedent('\n line 1\n line 2\n\n') == 'line 1\n line 2\n' assert dedent(''' Dedented, %s text ''', 'interpolated') == 'Dedented, interpolated text\n' assert dedent(''' Dedented, {op} text ''', op='formatted') == 'Dedented, formatted text\n' def test_pluralization(self): """Test :func:`humanfriendly.pluralize()`.""" self.assertEqual('1 word', humanfriendly.pluralize(1, 'word')) self.assertEqual('2 words', humanfriendly.pluralize(2, 'word')) self.assertEqual('1 box', humanfriendly.pluralize(1, 'box', 'boxes')) self.assertEqual('2 boxes', humanfriendly.pluralize(2, 'box', 'boxes')) def test_generate_slug(self): """Test :func:`humanfriendly.text.generate_slug()`.""" # Test the basic functionality. self.assertEqual('some-random-text', generate_slug('Some Random Text!')) # Test that previous output doesn't change. self.assertEqual('some-random-text', generate_slug('some-random-text')) # Test that inputs which can't be converted to a slug raise an exception. self.assertRaises(ValueError, generate_slug, ' ') self.assertRaises(ValueError, generate_slug, '-') def test_boolean_coercion(self): """Test :func:`humanfriendly.coerce_boolean()`.""" for value in [True, 'TRUE', 'True', 'true', 'on', 'yes', '1']: self.assertEqual(True, humanfriendly.coerce_boolean(value)) for value in [False, 'FALSE', 'False', 'false', 'off', 'no', '0']: self.assertEqual(False, humanfriendly.coerce_boolean(value)) self.assertRaises(ValueError, humanfriendly.coerce_boolean, 'not a boolean') def test_pattern_coercion(self): """Test :func:`humanfriendly.coerce_pattern()`.""" empty_pattern = re.compile('') # Make sure strings are converted to compiled regular expressions. assert isinstance(coerce_pattern('foobar'), type(empty_pattern)) # Make sure compiled regular expressions pass through untouched. assert empty_pattern is coerce_pattern(empty_pattern) # Make sure flags are respected. pattern = coerce_pattern('foobar', re.IGNORECASE) assert pattern.match('FOOBAR') # Make sure invalid values raise the expected exception. self.assertRaises(ValueError, coerce_pattern, []) def test_format_timespan(self): """Test :func:`humanfriendly.format_timespan()`.""" minute = 60 hour = minute * 60 day = hour * 24 week = day * 7 year = week * 52 assert '1 millisecond' == humanfriendly.format_timespan(0.001, detailed=True) assert '500 milliseconds' == humanfriendly.format_timespan(0.5, detailed=True) assert '0.5 seconds' == humanfriendly.format_timespan(0.5, detailed=False) assert '0 seconds' == humanfriendly.format_timespan(0) assert '0.54 seconds' == humanfriendly.format_timespan(0.54321) assert '1 second' == humanfriendly.format_timespan(1) assert '3.14 seconds' == humanfriendly.format_timespan(math.pi) assert '1 minute' == humanfriendly.format_timespan(minute) assert '1 minute and 20 seconds' == humanfriendly.format_timespan(80) assert '2 minutes' == humanfriendly.format_timespan(minute * 2) assert '1 hour' == humanfriendly.format_timespan(hour) assert '2 hours' == humanfriendly.format_timespan(hour * 2) assert '1 day' == humanfriendly.format_timespan(day) assert '2 days' == humanfriendly.format_timespan(day * 2) assert '1 week' == humanfriendly.format_timespan(week) assert '2 weeks' == humanfriendly.format_timespan(week * 2) assert '1 year' == humanfriendly.format_timespan(year) assert '2 years' == humanfriendly.format_timespan(year * 2) assert '6 years, 5 weeks, 4 days, 3 hours, 2 minutes and 500 milliseconds' == \ humanfriendly.format_timespan(year * 6 + week * 5 + day * 4 + hour * 3 + minute * 2 + 0.5, detailed=True) assert '1 year, 2 weeks and 3 days' == \ humanfriendly.format_timespan(year + week * 2 + day * 3 + hour * 12) # Make sure milliseconds are never shown separately when detailed=False. # https://github.com/xolox/python-humanfriendly/issues/10 assert '1 minute, 1 second and 100 milliseconds' == humanfriendly.format_timespan(61.10, detailed=True) assert '1 minute and 1.1 second' == humanfriendly.format_timespan(61.10, detailed=False) # Test for loss of precision as reported in issue 11: # https://github.com/xolox/python-humanfriendly/issues/11 assert '1 minute and 0.3 seconds' == humanfriendly.format_timespan(60.300) assert '5 minutes and 0.3 seconds' == humanfriendly.format_timespan(300.300) assert '1 second and 15 milliseconds' == humanfriendly.format_timespan(1.015, detailed=True) assert '10 seconds and 15 milliseconds' == humanfriendly.format_timespan(10.015, detailed=True) # Test the datetime.timedelta support: # https://github.com/xolox/python-humanfriendly/issues/27 now = datetime.datetime.now() then = now - datetime.timedelta(hours=23) assert '23 hours' == humanfriendly.format_timespan(now - then) def test_parse_timespan(self): """Test :func:`humanfriendly.parse_timespan()`.""" self.assertEqual(0, humanfriendly.parse_timespan('0')) self.assertEqual(0, humanfriendly.parse_timespan('0s')) self.assertEqual(0.001, humanfriendly.parse_timespan('1ms')) self.assertEqual(0.001, humanfriendly.parse_timespan('1 millisecond')) self.assertEqual(0.5, humanfriendly.parse_timespan('500 milliseconds')) self.assertEqual(0.5, humanfriendly.parse_timespan('0.5 seconds')) self.assertEqual(5, humanfriendly.parse_timespan('5s')) self.assertEqual(5, humanfriendly.parse_timespan('5 seconds')) self.assertEqual(60 * 2, humanfriendly.parse_timespan('2m')) self.assertEqual(60 * 2, humanfriendly.parse_timespan('2 minutes')) self.assertEqual(60 * 3, humanfriendly.parse_timespan('3 min')) self.assertEqual(60 * 3, humanfriendly.parse_timespan('3 mins')) self.assertEqual(60 * 60 * 3, humanfriendly.parse_timespan('3 h')) self.assertEqual(60 * 60 * 3, humanfriendly.parse_timespan('3 hours')) self.assertEqual(60 * 60 * 24 * 4, humanfriendly.parse_timespan('4d')) self.assertEqual(60 * 60 * 24 * 4, humanfriendly.parse_timespan('4 days')) self.assertEqual(60 * 60 * 24 * 7 * 5, humanfriendly.parse_timespan('5 w')) self.assertEqual(60 * 60 * 24 * 7 * 5, humanfriendly.parse_timespan('5 weeks')) self.assertRaises(humanfriendly.InvalidTimespan, humanfriendly.parse_timespan, '1z') def test_parse_date(self): """Test :func:`humanfriendly.parse_date()`.""" self.assertEqual((2013, 6, 17, 0, 0, 0), humanfriendly.parse_date('2013-06-17')) self.assertEqual((2013, 6, 17, 2, 47, 42), humanfriendly.parse_date('2013-06-17 02:47:42')) self.assertEqual((2016, 11, 30, 0, 47, 17), humanfriendly.parse_date(u'2016-11-30 00:47:17')) self.assertRaises(humanfriendly.InvalidDate, humanfriendly.parse_date, '2013-06-XY') def test_format_size(self): """Test :func:`humanfriendly.format_size()`.""" self.assertEqual('0 bytes', humanfriendly.format_size(0)) self.assertEqual('1 byte', humanfriendly.format_size(1)) self.assertEqual('42 bytes', humanfriendly.format_size(42)) self.assertEqual('1 KB', humanfriendly.format_size(1000 ** 1)) self.assertEqual('1 MB', humanfriendly.format_size(1000 ** 2)) self.assertEqual('1 GB', humanfriendly.format_size(1000 ** 3)) self.assertEqual('1 TB', humanfriendly.format_size(1000 ** 4)) self.assertEqual('1 PB', humanfriendly.format_size(1000 ** 5)) self.assertEqual('1 EB', humanfriendly.format_size(1000 ** 6)) self.assertEqual('1 ZB', humanfriendly.format_size(1000 ** 7)) self.assertEqual('1 YB', humanfriendly.format_size(1000 ** 8)) self.assertEqual('1 KiB', humanfriendly.format_size(1024 ** 1, binary=True)) self.assertEqual('1 MiB', humanfriendly.format_size(1024 ** 2, binary=True)) self.assertEqual('1 GiB', humanfriendly.format_size(1024 ** 3, binary=True)) self.assertEqual('1 TiB', humanfriendly.format_size(1024 ** 4, binary=True)) self.assertEqual('1 PiB', humanfriendly.format_size(1024 ** 5, binary=True)) self.assertEqual('1 EiB', humanfriendly.format_size(1024 ** 6, binary=True)) self.assertEqual('1 ZiB', humanfriendly.format_size(1024 ** 7, binary=True)) self.assertEqual('1 YiB', humanfriendly.format_size(1024 ** 8, binary=True)) self.assertEqual('45 KB', humanfriendly.format_size(1000 * 45)) self.assertEqual('2.9 TB', humanfriendly.format_size(1000 ** 4 * 2.9)) def test_parse_size(self): """Test :func:`humanfriendly.parse_size()`.""" self.assertEqual(0, humanfriendly.parse_size('0B')) self.assertEqual(42, humanfriendly.parse_size('42')) self.assertEqual(42, humanfriendly.parse_size('42B')) self.assertEqual(1000, humanfriendly.parse_size('1k')) self.assertEqual(1024, humanfriendly.parse_size('1k', binary=True)) self.assertEqual(1000, humanfriendly.parse_size('1 KB')) self.assertEqual(1000, humanfriendly.parse_size('1 kilobyte')) self.assertEqual(1024, humanfriendly.parse_size('1 kilobyte', binary=True)) self.assertEqual(1000 ** 2 * 69, humanfriendly.parse_size('69 MB')) self.assertEqual(1000 ** 3, humanfriendly.parse_size('1 GB')) self.assertEqual(1000 ** 4, humanfriendly.parse_size('1 TB')) self.assertEqual(1000 ** 5, humanfriendly.parse_size('1 PB')) self.assertEqual(1000 ** 6, humanfriendly.parse_size('1 EB')) self.assertEqual(1000 ** 7, humanfriendly.parse_size('1 ZB')) self.assertEqual(1000 ** 8, humanfriendly.parse_size('1 YB')) self.assertEqual(1000 ** 3 * 1.5, humanfriendly.parse_size('1.5 GB')) self.assertEqual(1024 ** 8 * 1.5, humanfriendly.parse_size('1.5 YiB')) self.assertRaises(humanfriendly.InvalidSize, humanfriendly.parse_size, '1q') self.assertRaises(humanfriendly.InvalidSize, humanfriendly.parse_size, 'a') def test_format_length(self): """Test :func:`humanfriendly.format_length()`.""" self.assertEqual('0 metres', humanfriendly.format_length(0)) self.assertEqual('1 metre', humanfriendly.format_length(1)) self.assertEqual('42 metres', humanfriendly.format_length(42)) self.assertEqual('1 km', humanfriendly.format_length(1 * 1000)) self.assertEqual('15.3 cm', humanfriendly.format_length(0.153)) self.assertEqual('1 cm', humanfriendly.format_length(1e-02)) self.assertEqual('1 mm', humanfriendly.format_length(1e-03)) self.assertEqual('1 nm', humanfriendly.format_length(1e-09)) def test_parse_length(self): """Test :func:`humanfriendly.parse_length()`.""" self.assertEqual(0, humanfriendly.parse_length('0m')) self.assertEqual(42, humanfriendly.parse_length('42')) self.assertEqual(42, humanfriendly.parse_length('42m')) self.assertEqual(1000, humanfriendly.parse_length('1km')) self.assertEqual(0.153, humanfriendly.parse_length('15.3 cm')) self.assertEqual(1e-02, humanfriendly.parse_length('1cm')) self.assertEqual(1e-03, humanfriendly.parse_length('1mm')) self.assertEqual(1e-09, humanfriendly.parse_length('1nm')) self.assertRaises(humanfriendly.InvalidLength, humanfriendly.parse_length, '1z') self.assertRaises(humanfriendly.InvalidLength, humanfriendly.parse_length, 'a') def test_format_number(self): """Test :func:`humanfriendly.format_number()`.""" self.assertEqual('1', humanfriendly.format_number(1)) self.assertEqual('1.5', humanfriendly.format_number(1.5)) self.assertEqual('1.56', humanfriendly.format_number(1.56789)) self.assertEqual('1.567', humanfriendly.format_number(1.56789, 3)) self.assertEqual('1,000', humanfriendly.format_number(1000)) self.assertEqual('1,000', humanfriendly.format_number(1000.12, 0)) self.assertEqual('1,000,000', humanfriendly.format_number(1000000)) self.assertEqual('1,000,000.42', humanfriendly.format_number(1000000.42)) def test_round_number(self): """Test :func:`humanfriendly.round_number()`.""" self.assertEqual('1', humanfriendly.round_number(1)) self.assertEqual('1', humanfriendly.round_number(1.0)) self.assertEqual('1.00', humanfriendly.round_number(1, keep_width=True)) self.assertEqual('3.14', humanfriendly.round_number(3.141592653589793)) def test_format_path(self): """Test :func:`humanfriendly.format_path()`.""" friendly_path = os.path.join('~', '.vimrc') absolute_path = os.path.join(os.environ['HOME'], '.vimrc') self.assertEqual(friendly_path, humanfriendly.format_path(absolute_path)) def test_parse_path(self): """Test :func:`humanfriendly.parse_path()`.""" friendly_path = os.path.join('~', '.vimrc') absolute_path = os.path.join(os.environ['HOME'], '.vimrc') self.assertEqual(absolute_path, humanfriendly.parse_path(friendly_path)) def test_pretty_tables(self): """Test :func:`humanfriendly.tables.format_pretty_table()`.""" # The simplest case possible :-). data = [['Just one column']] assert format_pretty_table(data) == dedent(""" ------------------- | Just one column | ------------------- """).strip() # A bit more complex: two rows, three columns, varying widths. data = [['One', 'Two', 'Three'], ['1', '2', '3']] assert format_pretty_table(data) == dedent(""" --------------------- | One | Two | Three | | 1 | 2 | 3 | --------------------- """).strip() # A table including column names. column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'c']] assert ansi_strip(format_pretty_table(data, column_names)) == dedent(""" --------------------- | One | Two | Three | --------------------- | 1 | 2 | 3 | | a | b | c | --------------------- """).strip() # A table that contains a column with only numeric data (will be right aligned). column_names = ['Just a label', 'Important numbers'] data = [['Row one', '15'], ['Row two', '300']] assert ansi_strip(format_pretty_table(data, column_names)) == dedent(""" ------------------------------------ | Just a label | Important numbers | ------------------------------------ | Row one | 15 | | Row two | 300 | ------------------------------------ """).strip() def test_robust_tables(self): """Test :func:`humanfriendly.tables.format_robust_table()`.""" column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'c']] assert ansi_strip(format_robust_table(data, column_names)) == dedent(""" -------- One: 1 Two: 2 Three: 3 -------- One: a Two: b Three: c -------- """).strip() column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']] assert ansi_strip(format_robust_table(data, column_names)) == dedent(""" ------------------ One: 1 Two: 2 Three: 3 ------------------ One: a Two: b Three: Here comes a multi line column! ------------------ """).strip() def test_smart_tables(self): """Test :func:`humanfriendly.tables.format_smart_table()`.""" column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'c']] assert ansi_strip(format_smart_table(data, column_names)) == dedent(""" --------------------- | One | Two | Three | --------------------- | 1 | 2 | 3 | | a | b | c | --------------------- """).strip() column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']] assert ansi_strip(format_smart_table(data, column_names)) == dedent(""" ------------------ One: 1 Two: 2 Three: 3 ------------------ One: a Two: b Three: Here comes a multi line column! ------------------ """).strip() def test_rst_tables(self): """Test :func:`humanfriendly.tables.format_rst_table()`.""" # Generate a table with column names. column_names = ['One', 'Two', 'Three'] data = [['1', '2', '3'], ['a', 'b', 'c']] self.assertEquals( format_rst_table(data, column_names), dedent(""" === === ===== One Two Three === === ===== 1 2 3 a b c === === ===== """).rstrip(), ) # Generate a table without column names. data = [['1', '2', '3'], ['a', 'b', 'c']] self.assertEquals( format_rst_table(data), dedent(""" = = = 1 2 3 a b c = = = """).rstrip(), ) def test_concatenate(self): """Test :func:`humanfriendly.concatenate()`.""" self.assertEqual(humanfriendly.concatenate([]), '') self.assertEqual(humanfriendly.concatenate(['one']), 'one') self.assertEqual(humanfriendly.concatenate(['one', 'two']), 'one and two') self.assertEqual(humanfriendly.concatenate(['one', 'two', 'three']), 'one, two and three') def test_split(self): """Test :func:`humanfriendly.text.split()`.""" from humanfriendly.text import split self.assertEqual(split(''), []) self.assertEqual(split('foo'), ['foo']) self.assertEqual(split('foo, bar'), ['foo', 'bar']) self.assertEqual(split('foo, bar, baz'), ['foo', 'bar', 'baz']) self.assertEqual(split('foo,bar,baz'), ['foo', 'bar', 'baz']) def test_timer(self): """Test :func:`humanfriendly.Timer`.""" for seconds, text in ((1, '1 second'), (2, '2 seconds'), (60, '1 minute'), (60 * 2, '2 minutes'), (60 * 60, '1 hour'), (60 * 60 * 2, '2 hours'), (60 * 60 * 24, '1 day'), (60 * 60 * 24 * 2, '2 days'), (60 * 60 * 24 * 7, '1 week'), (60 * 60 * 24 * 7 * 2, '2 weeks')): t = humanfriendly.Timer(time.time() - seconds) self.assertEqual(humanfriendly.round_number(t.elapsed_time, keep_width=True), '%i.00' % seconds) self.assertEqual(str(t), text) # Test rounding to seconds. t = humanfriendly.Timer(time.time() - 2.2) self.assertEqual(t.rounded, '2 seconds') # Test automatic timer. automatic_timer = humanfriendly.Timer() time.sleep(1) # XXX The following normalize_timestamp(ndigits=0) calls are intended # to compensate for unreliable clock sources in virtual machines # like those encountered on Travis CI, see also: # https://travis-ci.org/xolox/python-humanfriendly/jobs/323944263 self.assertEqual(normalize_timestamp(automatic_timer.elapsed_time, 0), '1.00') # Test resumable timer. resumable_timer = humanfriendly.Timer(resumable=True) for i in range(2): with resumable_timer: time.sleep(1) self.assertEqual(normalize_timestamp(resumable_timer.elapsed_time, 0), '2.00') # Make sure Timer.__enter__() returns the timer object. with humanfriendly.Timer(resumable=True) as timer: assert timer is not None def test_spinner(self): """Test :func:`humanfriendly.Spinner`.""" stream = StringIO() spinner = humanfriendly.Spinner('test spinner', total=4, stream=stream, interactive=True) for progress in [1, 2, 3, 4]: spinner.step(progress=progress) time.sleep(0.2) spinner.clear() output = stream.getvalue() output = (output.replace(humanfriendly.show_cursor_code, '') .replace(humanfriendly.hide_cursor_code, '')) lines = [line for line in output.split(humanfriendly.erase_line_code) if line] self.assertTrue(len(lines) > 0) self.assertTrue(all('test spinner' in l for l in lines)) self.assertTrue(all('%' in l for l in lines)) self.assertEqual(sorted(set(lines)), sorted(lines)) def test_automatic_spinner(self): """ Test :func:`humanfriendly.AutomaticSpinner`. There's not a lot to test about the :class:`.AutomaticSpinner` class, but by at least running it here we are assured that the code functions on all supported Python versions. :class:`.AutomaticSpinner` is built on top of the :class:`.Spinner` class so at least we also have the tests for the :class:`.Spinner` class to back us up. """ with humanfriendly.AutomaticSpinner('test spinner'): time.sleep(1) def test_prompt_for_choice(self): """Test :func:`humanfriendly.prompts.prompt_for_choice()`.""" # Choice selection without any options should raise an exception. self.assertRaises(ValueError, prompt_for_choice, []) # If there's only one option no prompt should be rendered so we expect # the following code to not raise an EOFError exception (despite # connecting standard input to /dev/null). with open(os.devnull) as handle: with PatchedAttribute(sys, 'stdin', handle): only_option = 'only one option (shortcut)' assert prompt_for_choice([only_option]) == only_option # Choice selection by full string match. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'foo'): assert prompt_for_choice(['foo', 'bar']) == 'foo' # Choice selection by substring input. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'f'): assert prompt_for_choice(['foo', 'bar']) == 'foo' # Choice selection by number. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: '2'): assert prompt_for_choice(['foo', 'bar']) == 'bar' # Choice selection by going with the default. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''): assert prompt_for_choice(['foo', 'bar'], default='bar') == 'bar' # Invalid substrings are refused. replies = ['', 'q', 'z'] with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)): assert prompt_for_choice(['foo', 'bar', 'baz']) == 'baz' # Choice selection by substring input requires an unambiguous substring match. replies = ['a', 'q'] with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)): assert prompt_for_choice(['foo', 'bar', 'baz', 'qux']) == 'qux' # Invalid numbers are refused. replies = ['42', '2'] with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)): assert prompt_for_choice(['foo', 'bar', 'baz']) == 'bar' # Test that interactive prompts eventually give up on invalid replies. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''): self.assertRaises(TooManyInvalidReplies, prompt_for_choice, ['a', 'b', 'c']) def test_prompt_for_confirmation(self): """Test :func:`humanfriendly.prompts.prompt_for_confirmation()`.""" # Test some (more or less) reasonable replies that indicate agreement. for reply in 'yes', 'Yes', 'YES', 'y', 'Y': with PatchedAttribute(prompts, 'interactive_prompt', lambda p: reply): assert prompt_for_confirmation("Are you sure?") is True # Test some (more or less) reasonable replies that indicate disagreement. for reply in 'no', 'No', 'NO', 'n', 'N': with PatchedAttribute(prompts, 'interactive_prompt', lambda p: reply): assert prompt_for_confirmation("Are you sure?") is False # Test that empty replies select the default choice. for default_choice in True, False: with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''): assert prompt_for_confirmation("Are you sure?", default=default_choice) is default_choice # Test that a warning is shown when no input nor a default is given. replies = ['', 'y'] with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)): with CaptureOutput() as capturer: assert prompt_for_confirmation("Are you sure?") is True assert "there's no default choice" in capturer.get_text() # Test that the default reply is shown in uppercase. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'y'): for default_value, expected_text in (True, 'Y/n'), (False, 'y/N'), (None, 'y/n'): with CaptureOutput() as capturer: assert prompt_for_confirmation("Are you sure?", default=default_value) is True assert expected_text in capturer.get_text() # Test that interactive prompts eventually give up on invalid replies. with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''): self.assertRaises(TooManyInvalidReplies, prompt_for_confirmation, "Are you sure?") def test_prompt_for_input(self): """Test :func:`humanfriendly.prompts.prompt_for_input()`.""" with open(os.devnull) as handle: with PatchedAttribute(sys, 'stdin', handle): # If standard input isn't connected to a terminal the default value should be returned. default_value = "To seek the holy grail!" assert prompt_for_input("What is your quest?", default=default_value) == default_value # If standard input isn't connected to a terminal and no default value # is given the EOFError exception should be propagated to the caller. self.assertRaises(EOFError, prompt_for_input, "What is your favorite color?") def test_cli(self): """Test the command line interface.""" # Test that the usage message is printed by default. returncode, output = run_cli(main) assert 'Usage:' in output # Test that the usage message can be requested explicitly. returncode, output = run_cli(main, '--help') assert 'Usage:' in output # Test handling of invalid command line options. returncode, output = run_cli(main, '--unsupported-option') assert returncode != 0 # Test `humanfriendly --format-number'. returncode, output = run_cli(main, '--format-number=1234567') assert output.strip() == '1,234,567' # Test `humanfriendly --format-size'. random_byte_count = random.randint(1024, 1024 * 1024) returncode, output = run_cli(main, '--format-size=%i' % random_byte_count) assert output.strip() == humanfriendly.format_size(random_byte_count) # Test `humanfriendly --format-size --binary'. random_byte_count = random.randint(1024, 1024 * 1024) returncode, output = run_cli(main, '--format-size=%i' % random_byte_count, '--binary') assert output.strip() == humanfriendly.format_size(random_byte_count, binary=True) # Test `humanfriendly --format-length'. random_len = random.randint(1024, 1024 * 1024) returncode, output = run_cli(main, '--format-length=%i' % random_len) assert output.strip() == humanfriendly.format_length(random_len) random_len = float(random_len) / 12345.6 returncode, output = run_cli(main, '--format-length=%f' % random_len) assert output.strip() == humanfriendly.format_length(random_len) # Test `humanfriendly --format-table'. returncode, output = run_cli(main, '--format-table', '--delimiter=\t', input='1\t2\t3\n4\t5\t6\n7\t8\t9') assert output.strip() == dedent(''' ------------- | 1 | 2 | 3 | | 4 | 5 | 6 | | 7 | 8 | 9 | ------------- ''').strip() # Test `humanfriendly --format-timespan'. random_timespan = random.randint(5, 600) returncode, output = run_cli(main, '--format-timespan=%i' % random_timespan) assert output.strip() == humanfriendly.format_timespan(random_timespan) # Test `humanfriendly --parse-size'. returncode, output = run_cli(main, '--parse-size=5 KB') assert int(output) == humanfriendly.parse_size('5 KB') # Test `humanfriendly --parse-size'. returncode, output = run_cli(main, '--parse-size=5 YiB') assert int(output) == humanfriendly.parse_size('5 YB', binary=True) # Test `humanfriendly --parse-length'. returncode, output = run_cli(main, '--parse-length=5 km') assert int(output) == humanfriendly.parse_length('5 km') returncode, output = run_cli(main, '--parse-length=1.05 km') assert float(output) == humanfriendly.parse_length('1.05 km') # Test `humanfriendly --run-command'. returncode, output = run_cli(main, '--run-command', 'bash', '-c', 'sleep 2 && exit 42') assert returncode == 42 # Test `humanfriendly --demo'. The purpose of this test is # to ensure that the demo runs successfully on all versions # of Python and outputs the expected sections (recognized by # their headings) without triggering exceptions. This was # written as a regression test after issue #28 was reported: # https://github.com/xolox/python-humanfriendly/issues/28 returncode, output = run_cli(main, '--demo') assert returncode == 0 lines = [ansi_strip(l) for l in output.splitlines()] assert "Text styles:" in lines assert "Foreground colors:" in lines assert "Background colors:" in lines assert "256 color mode (standard colors):" in lines assert "256 color mode (high-intensity colors):" in lines assert "256 color mode (216 colors):" in lines assert "256 color mode (gray scale colors):" in lines def test_ansi_style(self): """Test :func:`humanfriendly.terminal.ansi_style()`.""" assert ansi_style(bold=True) == '%s1%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(faint=True) == '%s2%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(italic=True) == '%s3%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(underline=True) == '%s4%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(inverse=True) == '%s7%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(strike_through=True) == '%s9%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(color='blue') == '%s34%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(background='blue') == '%s44%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(color='blue', bright=True) == '%s94%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(color=214) == '%s38;5;214%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(background=214) == '%s39;5;214%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(color=(0, 0, 0)) == '%s38;2;0;0;0%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(color=(255, 255, 255)) == '%s38;2;255;255;255%s' % (ANSI_CSI, ANSI_SGR) assert ansi_style(background=(50, 100, 150)) == '%s48;2;50;100;150%s' % (ANSI_CSI, ANSI_SGR) self.assertRaises(ValueError, ansi_style, color='unknown') def test_ansi_width(self): """Test :func:`humanfriendly.terminal.ansi_width()`.""" text = "Whatever" # Make sure ansi_width() works as expected on strings without ANSI escape sequences. assert len(text) == ansi_width(text) # Wrap a text in ANSI escape sequences and make sure ansi_width() treats it as expected. wrapped = ansi_wrap(text, bold=True) # Make sure ansi_wrap() changed the text. assert wrapped != text # Make sure ansi_wrap() added additional bytes. assert len(wrapped) > len(text) # Make sure the result of ansi_width() stays the same. assert len(text) == ansi_width(wrapped) def test_ansi_wrap(self): """Test :func:`humanfriendly.terminal.ansi_wrap()`.""" text = "Whatever" # Make sure ansi_wrap() does nothing when no keyword arguments are given. assert text == ansi_wrap(text) # Make sure ansi_wrap() starts the text with the CSI sequence. assert ansi_wrap(text, bold=True).startswith(ANSI_CSI) # Make sure ansi_wrap() ends the text by resetting the ANSI styles. assert ansi_wrap(text, bold=True).endswith(ANSI_RESET) def test_html_to_ansi(self): """Test the :func:`humanfriendly.terminal.html_to_ansi()` function.""" assert html_to_ansi("Just some plain text") == "Just some plain text" # Hyperlinks. assert html_to_ansi('python.org') == \ '\x1b[0m\x1b[4;94mpython.org\x1b[0m (\x1b[0m\x1b[4;94mhttps://python.org\x1b[0m)' # Make sure `mailto:' prefixes are stripped (they're not at all useful in a terminal). assert html_to_ansi('peter@peterodding.com') == \ '\x1b[0m\x1b[4;94mpeter@peterodding.com\x1b[0m' # Bold text. assert html_to_ansi("Let's try bold") == "Let's try \x1b[0m\x1b[1mbold\x1b[0m" assert html_to_ansi("Let's try bold") == \ "Let's try \x1b[0m\x1b[1mbold\x1b[0m" # Italic text. assert html_to_ansi("Let's try italic") == \ "Let's try \x1b[0m\x1b[3mitalic\x1b[0m" assert html_to_ansi("Let's try italic") == \ "Let's try \x1b[0m\x1b[3mitalic\x1b[0m" # Underlined text. assert html_to_ansi("Let's try underline") == \ "Let's try \x1b[0m\x1b[4munderline\x1b[0m" assert html_to_ansi("Let's try underline") == \ "Let's try \x1b[0m\x1b[4munderline\x1b[0m" # Strike-through text. assert html_to_ansi("Let's try strike-through") == \ "Let's try \x1b[0m\x1b[9mstrike-through\x1b[0m" assert html_to_ansi("Let's try strike-through") == \ "Let's try \x1b[0m\x1b[9mstrike-through\x1b[0m" # Pre-formatted text. assert html_to_ansi("Let's try pre-formatted") == \ "Let's try \x1b[0m\x1b[33mpre-formatted\x1b[0m" # Text colors (with a 6 digit hexadecimal color value). assert html_to_ansi("Let's try text colors") == \ "Let's try \x1b[0m\x1b[38;2;170;187;204mtext colors\x1b[0m" # Background colors (with an rgb(N, N, N) expression). assert html_to_ansi("Let's try background colors") == \ "Let's try \x1b[0m\x1b[48;2;50;50;50mbackground colors\x1b[0m" # Line breaks. assert html_to_ansi("Let's try some
line
breaks") == \ "Let's try some\nline\nbreaks" # Check that decimal entities are decoded. assert html_to_ansi("&") == "&" # Check that named entities are decoded. assert html_to_ansi("&") == "&" assert html_to_ansi(">") == ">" assert html_to_ansi("<") == "<" # Check that hexadecimal entities are decoded. assert html_to_ansi("&") == "&" # Check that the text callback is actually called. def callback(text): return text.replace(':wink:', ';-)') assert ':wink:' not in html_to_ansi(':wink:', callback=callback) # Check that the text callback doesn't process preformatted text. assert ':wink:' in html_to_ansi(':wink:', callback=callback) # Try a somewhat convoluted but nevertheless real life example from my # personal chat archives that causes humanfriendly releases 4.15 and # 4.15.1 to raise an exception. assert html_to_ansi(u''' Tweakers zit er idd nog steeds:

peter@peter-work> curl -s tweakers.net | grep -i hosting
<a href="http://www.true.nl/webhosting/" rel="external" id="true" title="Hosting door True"></a>
Hosting door <a href="http://www.true.nl/vps/" title="VPS hosting" rel="external">True ''') def test_generate_output(self): """Test the :func:`humanfriendly.terminal.output()` function.""" text = "Standard output generated by output()" with CaptureOutput(merged=False) as capturer: output(text) self.assertEqual([text], capturer.stdout.get_lines()) self.assertEqual([], self.ignore_coverage_warning(capturer)) def test_generate_message(self): """Test the :func:`humanfriendly.terminal.message()` function.""" text = "Standard error generated by message()" with CaptureOutput(merged=False) as capturer: message(text) self.assertEqual([], capturer.stdout.get_lines()) self.assertIn(text, self.ignore_coverage_warning(capturer)) def test_generate_warning(self): """Test the output(), message() and warning() functions.""" text = "Standard error generated by warning()" with CaptureOutput(merged=False) as capturer: warning(text) self.assertEqual([], capturer.stdout.get_lines()) self.assertEqual([ansi_wrap(text, color='red')], self.ignore_coverage_warning(capturer)) def ignore_coverage_warning(self, capturer): """ Filter out coverage.py warning from standard error. This is intended to remove the following line from the lines captured on the standard error stream: Coverage.py warning: No data was collected. (no-data-collected) """ return [line for line in capturer.stderr.get_lines() if 'no-data-collected' not in line] def test_clean_output(self): """Test :func:`humanfriendly.terminal.clean_terminal_output()`.""" # Simple output should pass through unharmed (single line). assert clean_terminal_output('foo') == ['foo'] # Simple output should pass through unharmed (multiple lines). assert clean_terminal_output('foo\nbar') == ['foo', 'bar'] # Carriage returns and preceding substrings are removed. assert clean_terminal_output('foo\rbar\nbaz') == ['bar', 'baz'] # Carriage returns move the cursor to the start of the line without erasing text. assert clean_terminal_output('aaa\rab') == ['aba'] # Backspace moves the cursor one position back without erasing text. assert clean_terminal_output('aaa\b\bb') == ['aba'] # Trailing empty lines should be stripped. assert clean_terminal_output('foo\nbar\nbaz\n\n\n') == ['foo', 'bar', 'baz'] def test_find_terminal_size(self): """Test :func:`humanfriendly.terminal.find_terminal_size()`.""" lines, columns = find_terminal_size() # We really can't assert any minimum or maximum values here because it # simply doesn't make any sense; it's impossible for me to anticipate # on what environments this test suite will run in the future. assert lines > 0 assert columns > 0 # The find_terminal_size_using_ioctl() function is the default # implementation and it will likely work fine. This makes it hard to # test the fall back code paths though. However there's an easy way to # make find_terminal_size_using_ioctl() fail ... saved_stdin = sys.stdin saved_stdout = sys.stdout saved_stderr = sys.stderr try: # What do you mean this is brute force?! ;-) sys.stdin = StringIO() sys.stdout = StringIO() sys.stderr = StringIO() # Now find_terminal_size_using_ioctl() should fail even though # find_terminal_size_using_stty() might work fine. lines, columns = find_terminal_size() assert lines > 0 assert columns > 0 # There's also an ugly way to make `stty size' fail: The # subprocess.Popen class uses os.execvp() underneath, so if we # clear the $PATH it will break. saved_path = os.environ['PATH'] try: os.environ['PATH'] = '' # Now find_terminal_size_using_stty() should fail. lines, columns = find_terminal_size() assert lines > 0 assert columns > 0 finally: os.environ['PATH'] = saved_path finally: sys.stdin = saved_stdin sys.stdout = saved_stdout sys.stderr = saved_stderr def test_terminal_capabilities(self): """Test the functions that check for terminal capabilities.""" for test_stream in connected_to_terminal, terminal_supports_colors: # This test suite should be able to run interactively as well as # non-interactively, so we can't expect or demand that standard streams # will always be connected to a terminal. Fortunately Capturer enables # us to fake it :-). for stream in sys.stdout, sys.stderr: with CaptureOutput(): assert test_stream(stream) # Test something that we know can never be a terminal. with open(os.devnull) as handle: assert not test_stream(handle) # Verify that objects without isatty() don't raise an exception. assert not test_stream(object()) def test_show_pager(self): """Test :func:`humanfriendly.terminal.show_pager()`.""" original_pager = os.environ.get('PAGER', None) try: # We specifically avoid `less' because it would become awkward to # run the test suite in an interactive terminal :-). os.environ['PAGER'] = 'cat' # Generate a significant amount of random text spread over multiple # lines that we expect to be reported literally on the terminal. random_text = "\n".join(random_string(25) for i in range(50)) # Run the pager command and validate the output. with CaptureOutput() as capturer: show_pager(random_text) assert random_text in capturer.get_text() finally: if original_pager is not None: # Restore the original $PAGER value. os.environ['PAGER'] = original_pager else: # Clear the custom $PAGER value. os.environ.pop('PAGER') def test_get_pager_command(self): """Test :func:`humanfriendly.terminal.get_pager_command()`.""" # Make sure --RAW-CONTROL-CHARS isn't used when it's not needed. assert '--RAW-CONTROL-CHARS' not in get_pager_command("Usage message") # Make sure --RAW-CONTROL-CHARS is used when it's needed. assert '--RAW-CONTROL-CHARS' in get_pager_command(ansi_wrap("Usage message", bold=True)) # Make sure that less-specific options are only used when valid. options_specific_to_less = ['--no-init', '--quit-if-one-screen'] for pager in 'cat', 'less': original_pager = os.environ.get('PAGER', None) try: # Set $PAGER to `cat' or `less'. os.environ['PAGER'] = pager # Get the pager command line. command_line = get_pager_command() # Check for less-specific options. if pager == 'less': assert all(opt in command_line for opt in options_specific_to_less) else: assert not any(opt in command_line for opt in options_specific_to_less) finally: if original_pager is not None: # Restore the original $PAGER value. os.environ['PAGER'] = original_pager else: # Clear the custom $PAGER value. os.environ.pop('PAGER') def test_find_meta_variables(self): """Test :func:`humanfriendly.usage.find_meta_variables()`.""" assert sorted(find_meta_variables(""" Here's one example: --format-number=VALUE Here's another example: --format-size=BYTES A final example: --format-timespan=SECONDS This line doesn't contain a META variable. """)) == sorted(['VALUE', 'BYTES', 'SECONDS']) def test_parse_usage_simple(self): """Test :func:`humanfriendly.usage.parse_usage()` (a simple case).""" introduction, options = self.preprocess_parse_result(""" Usage: my-fancy-app [OPTIONS] Boring description. Supported options: -h, --help Show this message and exit. """) # The following fragments are (expected to be) part of the introduction. assert "Usage: my-fancy-app [OPTIONS]" in introduction assert "Boring description." in introduction assert "Supported options:" in introduction # The following fragments are (expected to be) part of the documented options. assert "-h, --help" in options assert "Show this message and exit." in options def test_parse_usage_tricky(self): """Test :func:`humanfriendly.usage.parse_usage()` (a tricky case).""" introduction, options = self.preprocess_parse_result(""" Usage: my-fancy-app [OPTIONS] Here's the introduction to my-fancy-app. Some of the lines in the introduction start with a command line option just to confuse the parsing algorithm :-) For example --an-awesome-option is still part of the introduction. Supported options: -a, --an-awesome-option Explanation why this is an awesome option. -b, --a-boring-option Explanation why this is a boring option. """) # The following fragments are (expected to be) part of the introduction. assert "Usage: my-fancy-app [OPTIONS]" in introduction assert any('still part of the introduction' in p for p in introduction) assert "Supported options:" in introduction # The following fragments are (expected to be) part of the documented options. assert "-a, --an-awesome-option" in options assert "Explanation why this is an awesome option." in options assert "-b, --a-boring-option" in options assert "Explanation why this is a boring option." in options def test_parse_usage_commas(self): """Test :func:`humanfriendly.usage.parse_usage()` against option labels containing commas.""" introduction, options = self.preprocess_parse_result(""" Usage: my-fancy-app [OPTIONS] Some introduction goes here. Supported options: -f, --first-option Explanation of first option. -s, --second-option=WITH,COMMA This should be a separate option's description. """) # The following fragments are (expected to be) part of the introduction. assert "Usage: my-fancy-app [OPTIONS]" in introduction assert "Some introduction goes here." in introduction assert "Supported options:" in introduction # The following fragments are (expected to be) part of the documented options. assert "-f, --first-option" in options assert "Explanation of first option." in options assert "-s, --second-option=WITH,COMMA" in options assert "This should be a separate option's description." in options def preprocess_parse_result(self, text): """Ignore leading/trailing whitespace in usage parsing tests.""" return tuple([p.strip() for p in r] for r in parse_usage(dedent(text))) def test_format_usage(self): """Test :func:`humanfriendly.usage.format_usage()`.""" # Test that options are highlighted. usage_text = "Just one --option" formatted_text = format_usage(usage_text) assert len(formatted_text) > len(usage_text) assert formatted_text.startswith("Just one ") # Test that the "Usage: ..." line is highlighted. usage_text = "Usage: humanfriendly [OPTIONS]" formatted_text = format_usage(usage_text) assert len(formatted_text) > len(usage_text) assert usage_text in formatted_text assert not formatted_text.startswith(usage_text) # Test that meta variables aren't erroneously highlighted. usage_text = ( "--valid-option=VALID_METAVAR\n" "VALID_METAVAR is bogus\n" "INVALID_METAVAR should not be highlighted\n" ) formatted_text = format_usage(usage_text) formatted_lines = formatted_text.splitlines() # Make sure the meta variable in the second line is highlighted. assert ANSI_CSI in formatted_lines[1] # Make sure the meta variable in the third line isn't highlighted. assert ANSI_CSI not in formatted_lines[2] def test_render_usage(self): """Test :func:`humanfriendly.usage.render_usage()`.""" assert render_usage("Usage: some-command WITH ARGS") == "**Usage:** `some-command WITH ARGS`" assert render_usage("Supported options:") == "**Supported options:**" assert 'code-block' in render_usage(dedent(""" Here comes a shell command: $ echo test test """)) assert all(token in render_usage(dedent(""" Supported options: -n, --dry-run Don't change anything. """)) for token in ('`-n`', '`--dry-run`')) def test_sphinx_customizations(self): """Test the :mod:`humanfriendly.sphinx` module.""" class FakeApp(object): def __init__(self): self.callbacks = {} def __documented_special_method__(self): """Documented unofficial special method.""" pass def __undocumented_special_method__(self): # Intentionally not documented :-). pass def connect(self, event, callback): self.callbacks.setdefault(event, []).append(callback) def bogus_usage(self): """Usage: This is not supposed to be reformatted!""" pass # Test event callback registration. fake_app = FakeApp() setup(fake_app) assert special_methods_callback in fake_app.callbacks['autodoc-skip-member'] assert usage_message_callback in fake_app.callbacks['autodoc-process-docstring'] # Test that `special methods' which are documented aren't skipped. assert special_methods_callback( app=None, what=None, name=None, obj=FakeApp.__documented_special_method__, skip=True, options=None, ) is False # Test that `special methods' which are undocumented are skipped. assert special_methods_callback( app=None, what=None, name=None, obj=FakeApp.__undocumented_special_method__, skip=True, options=None, ) is True # Test formatting of usage messages. obj/lines from humanfriendly import cli, sphinx # We expect the docstring in the `cli' module to be reformatted # (because it contains a usage message in the expected format). assert self.docstring_is_reformatted(cli) # We don't expect the docstring in the `sphinx' module to be # reformatted (because it doesn't contain a usage message). assert not self.docstring_is_reformatted(sphinx) # We don't expect the docstring of the following *method* to be # reformatted because only *module* docstrings should be reformatted. assert not self.docstring_is_reformatted(fake_app.bogus_usage) def docstring_is_reformatted(self, entity): """Check whether :func:`.usage_message_callback()` reformats a module's docstring.""" lines = trim_empty_lines(entity.__doc__).splitlines() saved_lines = list(lines) usage_message_callback( app=None, what=None, name=None, obj=entity, options=None, lines=lines, ) return lines != saved_lines def normalize_timestamp(value, ndigits=1): """ Round timestamps to the given number of digits. This helps to make the test suite less sensitive to timing issues caused by multitasking, processor scheduling, etc. """ return '%.2f' % round(float(value), ndigits=ndigits) humanfriendly-4.18/humanfriendly/terminal.py0000644000175000017500000012135013330640326021627 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: August 2, 2018 # URL: https://humanfriendly.readthedocs.io """ Interaction with UNIX terminals. The :mod:`~humanfriendly.terminal` module makes it easy to interact with UNIX terminals and format text for rendering on UNIX terminals. If the terms used in the documentation of this module don't make sense to you then please refer to the `Wikipedia article on ANSI escape sequences`_ for details about how ANSI escape sequences work. .. _Wikipedia article on ANSI escape sequences: http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements """ # Standard library modules. import codecs import numbers import os import re import subprocess import sys # The `fcntl' module is platform specific so importing it may give an error. We # hide this implementation detail from callers by handling the import error and # setting a flag instead. try: import fcntl import termios import struct HAVE_IOCTL = True except ImportError: HAVE_IOCTL = False # Modules included in our package. We import find_meta_variables() here to # preserve backwards compatibility with older versions of humanfriendly where # that function was defined in this module. from humanfriendly.compat import HTMLParser, StringIO, coerce_string, name2codepoint, is_unicode, unichr from humanfriendly.text import compact_empty_lines, concatenate, format from humanfriendly.usage import find_meta_variables, format_usage # NOQA ANSI_CSI = '\x1b[' """The ANSI "Control Sequence Introducer" (a string).""" ANSI_SGR = 'm' """The ANSI "Select Graphic Rendition" sequence (a string).""" ANSI_ERASE_LINE = '%sK' % ANSI_CSI """The ANSI escape sequence to erase the current line (a string).""" ANSI_RESET = '%s0%s' % (ANSI_CSI, ANSI_SGR) """The ANSI escape sequence to reset styling (a string).""" ANSI_COLOR_CODES = dict(black=0, red=1, green=2, yellow=3, blue=4, magenta=5, cyan=6, white=7) """ A dictionary with (name, number) pairs of `portable color codes`_. Used by :func:`ansi_style()` to generate ANSI escape sequences that change font color. .. _portable color codes: http://en.wikipedia.org/wiki/ANSI_escape_code#Colors """ ANSI_TEXT_STYLES = dict(bold=1, faint=2, italic=3, underline=4, inverse=7, strike_through=9) """ A dictionary with (name, number) pairs of text styles (effects). Used by :func:`ansi_style()` to generate ANSI escape sequences that change text styles. Only widely supported text styles are included here. """ CLEAN_OUTPUT_PATTERN = re.compile(u'(\r|\n|\b|%s)' % re.escape(ANSI_ERASE_LINE)) """ A compiled regular expression used to separate significant characters from other text. This pattern is used by :func:`clean_terminal_output()` to split terminal output into regular text versus backspace, carriage return and line feed characters and ANSI 'erase line' escape sequences. """ DEFAULT_LINES = 25 """The default number of lines in a terminal (an integer).""" DEFAULT_COLUMNS = 80 """The default number of columns in a terminal (an integer).""" DEFAULT_ENCODING = 'UTF-8' """The output encoding for Unicode strings.""" HIGHLIGHT_COLOR = os.environ.get('HUMANFRIENDLY_HIGHLIGHT_COLOR', 'green') """ The color used to highlight important tokens in formatted text (e.g. the usage message of the ``humanfriendly`` program). If the environment variable ``$HUMANFRIENDLY_HIGHLIGHT_COLOR`` is set it determines the value of :data:`HIGHLIGHT_COLOR`. """ def output(text, *args, **kw): """ Print a formatted message to the standard output stream. For details about argument handling please refer to :func:`~humanfriendly.text.format()`. Renders the message using :func:`~humanfriendly.text.format()` and writes the resulting string (followed by a newline) to :data:`sys.stdout` using :func:`auto_encode()`. """ auto_encode(sys.stdout, coerce_string(text) + '\n', *args, **kw) def message(text, *args, **kw): """ Print a formatted message to the standard error stream. For details about argument handling please refer to :func:`~humanfriendly.text.format()`. Renders the message using :func:`~humanfriendly.text.format()` and writes the resulting string (followed by a newline) to :data:`sys.stderr` using :func:`auto_encode()`. """ auto_encode(sys.stderr, coerce_string(text) + '\n', *args, **kw) def warning(text, *args, **kw): """ Show a warning message on the terminal. For details about argument handling please refer to :func:`~humanfriendly.text.format()`. Renders the message using :func:`~humanfriendly.text.format()` and writes the resulting string (followed by a newline) to :data:`sys.stderr` using :func:`auto_encode()`. If :data:`sys.stderr` is connected to a terminal that supports colors, :func:`ansi_wrap()` is used to color the message in a red font (to make the warning stand out from surrounding text). """ text = coerce_string(text) if terminal_supports_colors(sys.stderr): text = ansi_wrap(text, color='red') auto_encode(sys.stderr, text + '\n', *args, **kw) def auto_encode(stream, text, *args, **kw): """ Reliably write Unicode strings to the terminal. :param stream: The file-like object to write to (a value like :data:`sys.stdout` or :data:`sys.stderr`). :param text: The text to write to the stream (a string). :param args: Refer to :func:`~humanfriendly.text.format()`. :param kw: Refer to :func:`~humanfriendly.text.format()`. Renders the text using :func:`~humanfriendly.text.format()` and writes it to the given stream. If an :exc:`~exceptions.UnicodeEncodeError` is encountered in doing so, the text is encoded using :data:`DEFAULT_ENCODING` and the write is retried. The reasoning behind this rather blunt approach is that it's preferable to get output on the command line in the wrong encoding then to have the Python program blow up with a :exc:`~exceptions.UnicodeEncodeError` exception. """ text = format(text, *args, **kw) try: stream.write(text) except UnicodeEncodeError: stream.write(codecs.encode(text, DEFAULT_ENCODING)) def ansi_strip(text, readline_hints=True): """ Strip ANSI escape sequences from the given string. :param text: The text from which ANSI escape sequences should be removed (a string). :param readline_hints: If :data:`True` then :func:`readline_strip()` is used to remove `readline hints`_ from the string. :returns: The text without ANSI escape sequences (a string). """ pattern = '%s.*?%s' % (re.escape(ANSI_CSI), re.escape(ANSI_SGR)) text = re.sub(pattern, '', text) if readline_hints: text = readline_strip(text) return text def ansi_style(**kw): """ Generate ANSI escape sequences for the given color and/or style(s). :param color: The foreground color. Three types of values are supported: - The name of a color (one of the strings 'black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan' or 'white'). - An integer that refers to the 256 color mode palette. - A tuple or list with three integers representing an RGB (red, green, blue) value. The value :data:`None` (the default) means no escape sequence to switch color will be emitted. :param background: The background color (see the description of the `color` argument). :param bright: Use high intensity colors instead of default colors (a boolean, defaults to :data:`False`). :param readline_hints: If :data:`True` then :func:`readline_wrap()` is applied to the generated ANSI escape sequences (the default is :data:`False`). :param kw: Any additional keyword arguments are expected to match a key in the :data:`ANSI_TEXT_STYLES` dictionary. If the argument's value evaluates to :data:`True` the respective style will be enabled. :returns: The ANSI escape sequences to enable the requested text styles or an empty string if no styles were requested. :raises: :exc:`~exceptions.ValueError` when an invalid color name is given. Even though only eight named colors are supported, the use of `bright=True` and `faint=True` increases the number of available colors to around 24 (it may be slightly lower, for example because faint black is just black). **Support for 8-bit colors** In `release 4.7`_ support for 256 color mode was added. While this significantly increases the available colors it's not very human friendly in usage because you need to look up color codes in the `256 color mode palette `_. You can use the ``humanfriendly --demo`` command to get a demonstration of the available colors, see also the screen shot below. Note that the small font size in the screen shot was so that the demonstration of 256 color mode support would fit into a single screen shot without scrolling :-) (I wasn't feeling very creative). .. image:: images/ansi-demo.png **Support for 24-bit colors** In `release 4.14`_ support for 24-bit colors was added by accepting a tuple or list with three integers representing the RGB (red, green, blue) value of a color. This is not included in the demo because rendering millions of colors was deemed unpractical ;-). .. _release 4.7: http://humanfriendly.readthedocs.io/en/latest/changelog.html#release-4-7-2018-01-14 .. _release 4.14: http://humanfriendly.readthedocs.io/en/latest/changelog.html#release-4-14-2018-07-13 """ # Start with sequences that change text styles. sequences = [ANSI_TEXT_STYLES[k] for k, v in kw.items() if k in ANSI_TEXT_STYLES and v] # Append the color code (if any). for color_type in 'color', 'background': color_value = kw.get(color_type) if isinstance(color_value, (tuple, list)): if len(color_value) != 3: msg = "Invalid color value %r! (expected tuple or list with three numbers)" raise ValueError(msg % color_value) sequences.append(48 if color_type == 'background' else 38) sequences.append(2) sequences.extend(map(int, color_value)) elif isinstance(color_value, numbers.Number): # Numeric values are assumed to be 256 color codes. sequences.extend(( 39 if color_type == 'background' else 38, 5, int(color_value) )) elif color_value: # Other values are assumed to be strings containing one of the known color names. if color_value not in ANSI_COLOR_CODES: msg = "Invalid color value %r! (expected an integer or one of the strings %s)" raise ValueError(msg % (color_value, concatenate(map(repr, sorted(ANSI_COLOR_CODES))))) # Pick the right offset for foreground versus background # colors and regular intensity versus bright colors. offset = ( (100 if kw.get('bright') else 40) if color_type == 'background' else (90 if kw.get('bright') else 30) ) # Combine the offset and color code into a single integer. sequences.append(offset + ANSI_COLOR_CODES[color_value]) if sequences: encoded = ANSI_CSI + ';'.join(map(str, sequences)) + ANSI_SGR return readline_wrap(encoded) if kw.get('readline_hints') else encoded else: return '' def ansi_width(text): """ Calculate the effective width of the given text (ignoring ANSI escape sequences). :param text: The text whose width should be calculated (a string). :returns: The width of the text without ANSI escape sequences (an integer). This function uses :func:`ansi_strip()` to strip ANSI escape sequences from the given string and returns the length of the resulting string. """ return len(ansi_strip(text)) def ansi_wrap(text, **kw): """ Wrap text in ANSI escape sequences for the given color and/or style(s). :param text: The text to wrap (a string). :param kw: Any keyword arguments are passed to :func:`ansi_style()`. :returns: The result of this function depends on the keyword arguments: - If :func:`ansi_style()` generates an ANSI escape sequence based on the keyword arguments, the given text is prefixed with the generated ANSI escape sequence and suffixed with :data:`ANSI_RESET`. - If :func:`ansi_style()` returns an empty string then the text given by the caller is returned unchanged. """ start_sequence = ansi_style(**kw) if start_sequence: end_sequence = ANSI_RESET if kw.get('readline_hints'): end_sequence = readline_wrap(end_sequence) return start_sequence + text + end_sequence else: return text def readline_wrap(expr): """ Wrap an ANSI escape sequence in `readline hints`_. :param text: The text with the escape sequence to wrap (a string). :returns: The wrapped text. .. _readline hints: http://superuser.com/a/301355 """ return '\001' + expr + '\002' def readline_strip(expr): """ Remove `readline hints`_ from a string. :param text: The text to strip (a string). :returns: The stripped text. """ return expr.replace('\001', '').replace('\002', '') def clean_terminal_output(text): """ Clean up the terminal output of a command. :param text: The raw text with special characters (a Unicode string). :returns: A list of Unicode strings (one for each line). This function emulates the effect of backspace (0x08), carriage return (0x0D) and line feed (0x0A) characters and the ANSI 'erase line' escape sequence on interactive terminals. It's intended to clean up command output that was originally meant to be rendered on an interactive terminal and that has been captured using e.g. the script_ program [#]_ or the :mod:`pty` module [#]_. .. [#] My coloredlogs_ package supports the ``coloredlogs --to-html`` command which uses script_ to fool a subprocess into thinking that it's connected to an interactive terminal (in order to get it to emit ANSI escape sequences). .. [#] My capturer_ package uses the :mod:`pty` module to fool the current process and subprocesses into thinking they are connected to an interactive terminal (in order to get them to emit ANSI escape sequences). **Some caveats about the use of this function:** - Strictly speaking the effect of carriage returns cannot be emulated outside of an actual terminal due to the interaction between overlapping output, terminal widths and line wrapping. The goal of this function is to sanitize noise in terminal output while preserving useful output. Think of it as a useful and pragmatic but possibly lossy conversion. - The algorithm isn't smart enough to properly handle a pair of ANSI escape sequences that open before a carriage return and close after the last carriage return in a linefeed delimited string; the resulting string will contain only the closing end of the ANSI escape sequence pair. Tracking this kind of complexity requires a state machine and proper parsing. .. _capturer: https://pypi.python.org/pypi/capturer .. _coloredlogs: https://pypi.python.org/pypi/coloredlogs .. _script: http://man7.org/linux/man-pages/man1/script.1.html """ cleaned_lines = [] current_line = '' current_position = 0 for token in CLEAN_OUTPUT_PATTERN.split(text): if token == '\r': # Seek back to the start of the current line. current_position = 0 elif token == '\b': # Seek back one character in the current line. current_position = max(0, current_position - 1) else: if token == '\n': # Capture the current line. cleaned_lines.append(current_line) if token in ('\n', ANSI_ERASE_LINE): # Clear the current line. current_line = '' current_position = 0 elif token: # Merge regular output into the current line. new_position = current_position + len(token) prefix = current_line[:current_position] suffix = current_line[new_position:] current_line = prefix + token + suffix current_position = new_position # Capture the last line (if any). cleaned_lines.append(current_line) # Remove any empty trailing lines. while cleaned_lines and not cleaned_lines[-1]: cleaned_lines.pop(-1) return cleaned_lines def connected_to_terminal(stream=None): """ Check if a stream is connected to a terminal. :param stream: The stream to check (a file-like object, defaults to :data:`sys.stdout`). :returns: :data:`True` if the stream is connected to a terminal, :data:`False` otherwise. See also :func:`terminal_supports_colors()`. """ stream = sys.stdout if stream is None else stream try: return stream.isatty() except Exception: return False def html_to_ansi(data, callback=None): """ Convert HTML with simple text formatting to text with ANSI escape sequences. :param data: The HTML to convert (a string). :param callback: Optional callback to pass to :class:`HTMLConverter`. :returns: Text with ANSI escape sequences (a string). Please refer to the documentation of the :class:`HTMLConverter` class for details about the conversion process (like which tags are supported) and an example with a screenshot. """ converter = HTMLConverter(callback=callback) return converter(data) def terminal_supports_colors(stream=None): """ Check if a stream is connected to a terminal that supports ANSI escape sequences. :param stream: The stream to check (a file-like object, defaults to :data:`sys.stdout`). :returns: :data:`True` if the terminal supports ANSI escape sequences, :data:`False` otherwise. This function is inspired by the implementation of `django.core.management.color.supports_color() `_. """ return (sys.platform != 'Pocket PC' and (sys.platform != 'win32' or 'ANSICON' in os.environ) and connected_to_terminal(stream)) def find_terminal_size(): """ Determine the number of lines and columns visible in the terminal. :returns: A tuple of two integers with the line and column count. The result of this function is based on the first of the following three methods that works: 1. First :func:`find_terminal_size_using_ioctl()` is tried, 2. then :func:`find_terminal_size_using_stty()` is tried, 3. finally :data:`DEFAULT_LINES` and :data:`DEFAULT_COLUMNS` are returned. .. note:: The :func:`find_terminal_size()` function performs the steps above every time it is called, the result is not cached. This is because the size of a virtual terminal can change at any time and the result of :func:`find_terminal_size()` should be correct. `Pre-emptive snarky comment`_: It's possible to cache the result of this function and use :data:`signal.SIGWINCH` to refresh the cached values! Response: As a library I don't consider it the role of the :mod:`humanfriendly.terminal` module to install a process wide signal handler ... .. _Pre-emptive snarky comment: http://blogs.msdn.com/b/oldnewthing/archive/2008/01/30/7315957.aspx """ # The first method. Any of the standard streams may have been redirected # somewhere and there's no telling which, so we'll just try them all. for stream in sys.stdin, sys.stdout, sys.stderr: try: result = find_terminal_size_using_ioctl(stream) if min(result) >= 1: return result except Exception: pass # The second method. try: result = find_terminal_size_using_stty() if min(result) >= 1: return result except Exception: pass # Fall back to conservative defaults. return DEFAULT_LINES, DEFAULT_COLUMNS def find_terminal_size_using_ioctl(stream): """ Find the terminal size using :func:`fcntl.ioctl()`. :param stream: A stream connected to the terminal (a file object with a ``fileno`` attribute). :returns: A tuple of two integers with the line and column count. :raises: This function can raise exceptions but I'm not going to document them here, you should be using :func:`find_terminal_size()`. Based on an `implementation found on StackOverflow `_. """ if not HAVE_IOCTL: raise NotImplementedError("It looks like the `fcntl' module is not available!") h, w, hp, wp = struct.unpack('HHHH', fcntl.ioctl(stream, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) return h, w def find_terminal_size_using_stty(): """ Find the terminal size using the external command ``stty size``. :param stream: A stream connected to the terminal (a file object). :returns: A tuple of two integers with the line and column count. :raises: This function can raise exceptions but I'm not going to document them here, you should be using :func:`find_terminal_size()`. """ stty = subprocess.Popen(['stty', 'size'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = stty.communicate() tokens = stdout.split() if len(tokens) != 2: raise Exception("Invalid output from `stty size'!") return tuple(map(int, tokens)) def usage(usage_text): """ Print a human friendly usage message to the terminal. :param text: The usage message to print (a string). This function does two things: 1. If :data:`sys.stdout` is connected to a terminal (see :func:`connected_to_terminal()`) then the usage message is formatted using :func:`.format_usage()`. 2. The usage message is shown using a pager (see :func:`show_pager()`). """ if terminal_supports_colors(sys.stdout): usage_text = format_usage(usage_text) show_pager(usage_text) def show_pager(formatted_text, encoding=DEFAULT_ENCODING): """ Print a large text to the terminal using a pager. :param formatted_text: The text to print to the terminal (a string). :param encoding: The name of the text encoding used to encode the formatted text if the formatted text is a Unicode string (a string, defaults to :data:`DEFAULT_ENCODING`). When :func:`connected_to_terminal()` returns :data:`True` a pager is used to show the text on the terminal, otherwise the text is printed directly without invoking a pager. The use of a pager helps to avoid the wall of text effect where the user has to scroll up to see where the output began (not very user friendly). Refer to :func:`get_pager_command()` for details about the command line that's used to invoke the pager. """ if connected_to_terminal(): command_line = get_pager_command(formatted_text) pager = subprocess.Popen(command_line, stdin=subprocess.PIPE) if is_unicode(formatted_text): formatted_text = formatted_text.encode(encoding) pager.communicate(input=formatted_text) else: output(formatted_text) def get_pager_command(text=None): """ Get the command to show a text on the terminal using a pager. :param text: The text to print to the terminal (a string). :returns: A list of strings with the pager command and arguments. The use of a pager helps to avoid the wall of text effect where the user has to scroll up to see where the output began (not very user friendly). If the given text contains ANSI escape sequences the command ``less --RAW-CONTROL-CHARS`` is used, otherwise the environment variable ``$PAGER`` is used (if ``$PAGER`` isn't set less_ is used). When the selected pager is less_, the following options are used to make the experience more user friendly: - ``--quit-if-one-screen`` causes less_ to automatically exit if the entire text can be displayed on the first screen. This makes the use of a pager transparent for smaller texts (because the operator doesn't have to quit the pager). - ``--no-init`` prevents less_ from clearing the screen when it exits. This ensures that the operator gets a chance to review the text (for example a usage message) after quitting the pager, while composing the next command. .. _less: http://man7.org/linux/man-pages/man1/less.1.html """ # Compose the pager command. if text and ANSI_CSI in text: command_line = ['less', '--RAW-CONTROL-CHARS'] else: command_line = [os.environ.get('PAGER', 'less')] # Pass some additional options to `less' (to make it more # user friendly) without breaking support for other pagers. if os.path.basename(command_line[0]) == 'less': command_line.append('--no-init') command_line.append('--quit-if-one-screen') return command_line class HTMLConverter(HTMLParser): """ Convert HTML with simple text formatting to text with ANSI escape sequences. The following text styles are supported: - Bold: ````, ```` and ```` - Italic: ````, ```` and ```` - Strike-through: ````, ```` and ```` - Underline: ````, ```` and ```` Colors can be specified as follows: - Foreground color: ```` - Background color: ```` Here's a small demonstration: .. code-block:: python from humanfriendly.text import dedent from humanfriendly.terminal import html_to_ansi print(html_to_ansi(dedent(''' Hello world! Is this thing on? I guess I can underline or strike-through text? And what about color? '''))) rainbow_colors = [ '#FF0000', '#E2571E', '#FF7F00', '#FFFF00', '#00FF00', '#96BF33', '#0000FF', '#4B0082', '#8B00FF', '#FFFFFF', ] html_rainbow = "".join('o' % c for c in rainbow_colors) print(html_to_ansi("Let's try a rainbow: %s" % html_rainbow)) Here's what the results look like: .. image:: images/html-to-ansi.png Some more details: - Nested tags are supported, within reasonable limits. - Text in ```` and ``
`` tags will be highlighted in a
      different color from the main text (currently this is yellow).

    - ``TEXT`` is converted to the format "TEXT (URL)" where
      the uppercase symbols are highlighted in light blue with an underline.

    - ``
``, ``

`` and ``

`` tags are considered block level tags
      and are wrapped in vertical whitespace to prevent their content from
      "running into" surrounding text. This may cause runs of multiple empty
      lines to be emitted. As a *workaround* the :func:`__call__()` method
      will automatically call :func:`.compact_empty_lines()` on the generated
      output before returning it to the caller. Of course this won't work
      when `output` is set to something like :data:`sys.stdout`.

    - ``
`` is converted to a single plain text line break. Implementation notes: - A list of dictionaries with style information is used as a stack where new styling can be pushed and a pop will restore the previous styling. When new styling is pushed, it is merged with (but overrides) the current styling. - If you're going to be converting a lot of HTML it might be useful from a performance standpoint to re-use an existing :class:`HTMLConverter` object for unrelated HTML fragments, in this case take a look at the :func:`__call__()` method (it makes this use case very easy). .. versionadded:: 4.15 :class:`humanfriendly.terminal.HTMLConverter` was added to the `humanfriendly` package during the initial development of my new `chat-archive `_ project, whose command line interface makes for a great demonstration of the flexibility that this feature provides (hint: check out how the search keyword highlighting combines with the regular highlighting). """ BLOCK_TAGS = ('div', 'p', 'pre') """The names of tags that are padded with vertical whitespace.""" def __init__(self, *args, **kw): """ Initialize an :class:`HTMLConverter` object. :param callback: Optional keyword argument to specify a function that will be called to process text fragments before they are emitted on the output stream. Note that link text and preformatted text fragments are not processed by this callback. :param output: Optional keyword argument to redirect the output to the given file-like object. If this is not given a new :class:`python3:~io.StringIO` object is created. """ # Hide our optional keyword arguments from the superclass. self.callback = kw.pop("callback", None) self.output = kw.pop("output", None) # Initialize the superclass. HTMLParser.__init__(self, *args, **kw) def __call__(self, data): """ Reset the parser, convert some HTML and get the text with ANSI escape sequences. :param data: The HTML to convert to text (a string). :returns: The converted text (only in case `output` is a :class:`~python3:io.StringIO` object). """ self.reset() self.feed(data) self.close() if isinstance(self.output, StringIO): return compact_empty_lines(self.output.getvalue()) @property def current_style(self): """Get the current style from the top of the stack (a dictionary).""" return self.stack[-1] if self.stack else {} def close(self): """ Close previously opened ANSI escape sequences. This method overrides the same method in the superclass to ensure that an :data:`.ANSI_RESET` code is emitted when parsing reaches the end of the input but a style is still active. This is intended to prevent malformed HTML from messing up terminal output. """ if any(self.stack): self.output.write(ANSI_RESET) self.stack = [] HTMLParser.close(self) def emit_style(self, style=None): """ Emit an ANSI escape sequence for the given or current style to the output stream. :param style: A dictionary with arguments for :func:`ansi_style()` or :data:`None`, in which case the style at the top of the stack is emitted. """ # Clear the current text styles. self.output.write(ANSI_RESET) # Apply a new text style? style = self.current_style if style is None else style if style: self.output.write(ansi_style(**style)) def handle_charref(self, value): """ Process a decimal or hexadecimal numeric character reference. :param value: The decimal or hexadecimal value (a string). """ self.output.write(unichr(int(value[1:], 16) if value.startswith('x') else int(value))) def handle_data(self, data): """ Process textual data. :param data: The decoded text (a string). """ if self.link_url: # Link text is captured literally so that we can reliably check # whether the text and the URL of the link are the same string. self.link_text = data elif self.callback and self.preformatted_text_level == 0: # Text that is not part of a link and not preformatted text is # passed to the user defined callback to allow for arbitrary # pre-processing. data = self.callback(data) # All text is emitted unmodified on the output stream. self.output.write(data) def handle_endtag(self, tag): """ Process the end of an HTML tag. :param tag: The name of the tag (a string). """ if tag in ('a', 'b', 'code', 'del', 'em', 'i', 'ins', 'pre', 's', 'strong', 'span', 'u'): old_style = self.current_style # The following conditional isn't necessary for well formed # HTML but prevents raising exceptions on malformed HTML. if self.stack: self.stack.pop(-1) new_style = self.current_style if tag == 'a': if self.urls_match(self.link_text, self.link_url): # Don't render the URL when it's part of the link text. self.emit_style(new_style) else: self.emit_style(new_style) self.output.write(' (') self.emit_style(old_style) self.output.write(self.render_url(self.link_url)) self.emit_style(new_style) self.output.write(')') else: self.emit_style(new_style) if tag in ('code', 'pre'): self.preformatted_text_level -= 1 if tag in self.BLOCK_TAGS: # Emit an empty line after block level tags. self.output.write('\n\n') def handle_entityref(self, name): """ Process a named character reference. :param name: The name of the character reference (a string). """ self.output.write(unichr(name2codepoint[name])) def handle_starttag(self, tag, attrs): """ Process the start of an HTML tag. :param tag: The name of the tag (a string). :param attrs: A list of tuples with two strings each. """ if tag in self.BLOCK_TAGS: # Emit an empty line before block level tags. self.output.write('\n\n') if tag == 'a': self.push_styles(color='blue', bright=True, underline=True) # Store the URL that the link points to for later use, so that we # can render the link text before the URL (with the reasoning that # this is the most intuitive way to present a link in a plain text # interface). self.link_url = next((v for n, v in attrs if n == 'href'), '') elif tag == 'b' or tag == 'strong': self.push_styles(bold=True) elif tag == 'br': self.output.write('\n') elif tag == 'code' or tag == 'pre': self.push_styles(color='yellow') self.preformatted_text_level += 1 elif tag == 'del' or tag == 's': self.push_styles(strike_through=True) elif tag == 'em' or tag == 'i': self.push_styles(italic=True) elif tag == 'ins' or tag == 'u': self.push_styles(underline=True) elif tag == 'span': styles = {} css = next((v for n, v in attrs if n == 'style'), "") for rule in css.split(';'): name, _, value = rule.partition(':') name = name.strip() value = value.strip() if name == 'background-color': styles['background'] = self.parse_color(value) elif name == 'color': styles['color'] = self.parse_color(value) elif name == 'font-style' and value == 'italic': styles['italic'] = True elif name == 'font-weight' and value == 'bold': styles['bold'] = True elif name == 'text-decoration' and value == 'line-through': styles['strike_through'] = True elif name == 'text-decoration' and value == 'underline': styles['underline'] = True self.push_styles(**styles) def normalize_url(self, url): """ Normalize a URL to enable string equality comparison. :param url: The URL to normalize (a string). :returns: The normalized URL (a string). """ return re.sub('^mailto:', '', url) def parse_color(self, value): """ Convert a CSS color to something that :func:`ansi_style()` understands. :param value: A string like ``rgb(1,2,3)``, ``#AABBCC`` or ``yellow``. :returns: A color value supported by :func:`ansi_style()` or :data:`None`. """ # Parse an 'rgb(N,N,N)' expression. if value.startswith('rgb'): tokens = re.findall(r'\d+', value) if len(tokens) == 3: return tuple(map(int, tokens)) # Parse an '#XXXXXX' expression. elif value.startswith('#'): value = value[1:] length = len(value) if length == 6: # Six hex digits (proper notation). return ( int(value[:2], 16), int(value[2:4], 16), int(value[4:6], 16), ) elif length == 3: # Three hex digits (shorthand). return ( int(value[0], 16), int(value[1], 16), int(value[2], 16), ) # Try to recognize a named color. value = value.lower() if value in ANSI_COLOR_CODES: return value def push_styles(self, **changes): """ Push new style information onto the stack. :param changes: Any keyword arguments are passed on to :func:`.ansi_style()`. This method is a helper for :func:`handle_starttag()` that does the following: 1. Make a copy of the current styles (from the top of the stack), 2. Apply the given `changes` to the copy of the current styles, 3. Add the new styles to the stack, 4. Emit the appropriate ANSI escape sequence to the output stream. """ prototype = self.current_style if prototype: new_style = dict(prototype) new_style.update(changes) else: new_style = changes self.stack.append(new_style) self.emit_style(new_style) def render_url(self, url): """ Prepare a URL for rendering on the terminal. :param url: The URL to simplify (a string). :returns: The simplified URL (a string). This method pre-processes a URL before rendering on the terminal. The following modifications are made: - The ``mailto:`` prefix is stripped. - Spaces are converted to ``%20``. - A trailing parenthesis is converted to ``%29``. """ url = re.sub('^mailto:', '', url) url = re.sub(' ', '%20', url) url = re.sub(r'\)$', '%29', url) return url def reset(self): """ Reset the state of the HTML parser and ANSI converter. When `output` is a :class:`~python3:io.StringIO` object a new instance will be created (and the old one garbage collected). """ # Reset the state of the superclass. HTMLParser.reset(self) # Reset our instance variables. self.link_text = None self.link_url = None self.preformatted_text_level = 0 if self.output is None or isinstance(self.output, StringIO): # If the caller specified something like output=sys.stdout then it # doesn't make much sense to negate that choice here in reset(). self.output = StringIO() self.stack = [] def urls_match(self, a, b): """ Compare two URLs for equality using :func:`normalize_url()`. :param a: A string containing a URL. :param b: A string containing a URL. :returns: :data:`True` if the URLs are the same, :data:`False` otherwise. This method is used by :func:`handle_endtag()` to omit the URL of a hyperlink (````) when the link text is that same URL. """ return self.normalize_url(a) == self.normalize_url(b) humanfriendly-4.18/humanfriendly/prompts.py0000664000175000017500000003667713125024327021542 0ustar peterpeter00000000000000# vim: fileencoding=utf-8 # Human friendly input/output in Python. # # Author: Peter Odding # Last Change: June 24, 2017 # URL: https://humanfriendly.readthedocs.io """ Interactive terminal prompts. The :mod:`~humanfriendly.prompts` module enables interaction with the user (operator) by asking for confirmation (:func:`prompt_for_confirmation()`) and asking to choose from a list of options (:func:`prompt_for_choice()`). It works by rendering interactive prompts on the terminal. """ # Standard library modules. import logging import sys # Modules included in our package. from humanfriendly.compat import interactive_prompt from humanfriendly.terminal import ( HIGHLIGHT_COLOR, ansi_strip, ansi_wrap, connected_to_terminal, terminal_supports_colors, warning, ) from humanfriendly.text import format, concatenate MAX_ATTEMPTS = 10 """The number of times an interactive prompt is shown on invalid input (an integer).""" # Initialize a logger for this module. logger = logging.getLogger(__name__) def prompt_for_confirmation(question, default=None, padding=True): """ Prompt the user for confirmation. :param question: The text that explains what the user is confirming (a string). :param default: The default value (a boolean) or :data:`None`. :param padding: Refer to the documentation of :func:`prompt_for_input()`. :returns: - If the user enters 'yes' or 'y' then :data:`True` is returned. - If the user enters 'no' or 'n' then :data:`False` is returned. - If the user doesn't enter any text or standard input is not connected to a terminal (which makes it impossible to prompt the user) the value of the keyword argument ``default`` is returned (if that value is not :data:`None`). :raises: - Any exceptions raised by :func:`retry_limit()`. - Any exceptions raised by :func:`prompt_for_input()`. When `default` is :data:`False` and the user doesn't enter any text an error message is printed and the prompt is repeated: >>> prompt_for_confirmation("Are you sure?") Are you sure? [y/n] Error: Please enter 'yes' or 'no' (there's no default choice). Are you sure? [y/n] The same thing happens when the user enters text that isn't recognized: >>> prompt_for_confirmation("Are you sure?") Are you sure? [y/n] about what? Error: Please enter 'yes' or 'no' (the text 'about what?' is not recognized). Are you sure? [y/n] """ # Generate the text for the prompt. prompt_text = prepare_prompt_text(question, bold=True) # Append the valid replies (and default reply) to the prompt text. hint = "[Y/n]" if default else "[y/N]" if default is not None else "[y/n]" prompt_text += " %s " % prepare_prompt_text(hint, color=HIGHLIGHT_COLOR) # Loop until a valid response is given. logger.debug("Requesting interactive confirmation from terminal: %r", ansi_strip(prompt_text).rstrip()) for attempt in retry_limit(): reply = prompt_for_input(prompt_text, '', padding=padding, strip=True) if reply.lower() in ('y', 'yes'): logger.debug("Confirmation granted by reply (%r).", reply) return True elif reply.lower() in ('n', 'no'): logger.debug("Confirmation denied by reply (%r).", reply) return False elif (not reply) and default is not None: logger.debug("Default choice selected by empty reply (%r).", "granted" if default else "denied") return default else: details = ("the text '%s' is not recognized" % reply if reply else "there's no default choice") logger.debug("Got %s reply (%s), retrying (%i/%i) ..", "invalid" if reply else "empty", details, attempt, MAX_ATTEMPTS) warning("{indent}Error: Please enter 'yes' or 'no' ({details}).", indent=' ' if padding else '', details=details) def prompt_for_choice(choices, default=None, padding=True): """ Prompt the user to select a choice from a group of options. :param choices: A sequence of strings with available options. :param default: The default choice if the user simply presses Enter (expected to be a string, defaults to :data:`None`). :param padding: Refer to the documentation of :func:`prompt_for_input()`. :returns: The string corresponding to the user's choice. :raises: - :exc:`~exceptions.ValueError` if `choices` is an empty sequence. - Any exceptions raised by :func:`retry_limit()`. - Any exceptions raised by :func:`prompt_for_input()`. When no options are given an exception is raised: >>> prompt_for_choice([]) Traceback (most recent call last): File "humanfriendly/prompts.py", line 148, in prompt_for_choice raise ValueError("Can't prompt for choice without any options!") ValueError: Can't prompt for choice without any options! If a single option is given the user isn't prompted: >>> prompt_for_choice(['only one choice']) 'only one choice' Here's what the actual prompt looks like by default: >>> prompt_for_choice(['first option', 'second option']) 1. first option 2. second option Enter your choice as a number or unique substring (Control-C aborts): second 'second option' If you don't like the whitespace (empty lines and indentation): >>> prompt_for_choice(['first option', 'second option'], padding=False) 1. first option 2. second option Enter your choice as a number or unique substring (Control-C aborts): first 'first option' """ indent = ' ' if padding else '' # Make sure we can use 'choices' more than once (i.e. not a generator). choices = list(choices) if len(choices) == 1: # If there's only one option there's no point in prompting the user. logger.debug("Skipping interactive prompt because there's only option (%r).", choices[0]) return choices[0] elif not choices: # We can't render a choice prompt without any options. raise ValueError("Can't prompt for choice without any options!") # Generate the prompt text. prompt_text = ('\n\n' if padding else '\n').join([ # Present the available choices in a user friendly way. "\n".join([ (u" %i. %s" % (i, choice)) + (" (default choice)" if choice == default else "") for i, choice in enumerate(choices, start=1) ]), # Instructions for the user. "Enter your choice as a number or unique substring (Control-C aborts): ", ]) prompt_text = prepare_prompt_text(prompt_text, bold=True) # Loop until a valid choice is made. logger.debug("Requesting interactive choice on terminal (options are %s) ..", concatenate(map(repr, choices))) for attempt in retry_limit(): reply = prompt_for_input(prompt_text, '', padding=padding, strip=True) if not reply and default is not None: logger.debug("Default choice selected by empty reply (%r).", default) return default elif reply.isdigit(): index = int(reply) - 1 if 0 <= index < len(choices): logger.debug("Option (%r) selected by numeric reply (%s).", choices[index], reply) return choices[index] # Check for substring matches. matches = [] for choice in choices: lower_reply = reply.lower() lower_choice = choice.lower() if lower_reply == lower_choice: # If we have an 'exact' match we return it immediately. logger.debug("Option (%r) selected by reply (exact match).", choice) return choice elif lower_reply in lower_choice and len(lower_reply) > 0: # Otherwise we gather substring matches. matches.append(choice) if len(matches) == 1: # If a single choice was matched we return it. logger.debug("Option (%r) selected by reply (substring match on %r).", matches[0], reply) return matches[0] else: # Give the user a hint about what went wrong. if matches: details = format("text '%s' matches more than one choice: %s", reply, concatenate(matches)) elif reply.isdigit(): details = format("number %i is not a valid choice", int(reply)) elif reply and not reply.isspace(): details = format("text '%s' doesn't match any choices", reply) else: details = "there's no default choice" logger.debug("Got %s reply (%s), retrying (%i/%i) ..", "invalid" if reply else "empty", details, attempt, MAX_ATTEMPTS) warning("%sError: Invalid input (%s).", indent, details) def prompt_for_input(question, default=None, padding=True, strip=True): """ Prompt the user for input (free form text). :param question: An explanation of what is expected from the user (a string). :param default: The return value if the user doesn't enter any text or standard input is not connected to a terminal (which makes it impossible to prompt the user). :param padding: Render empty lines before and after the prompt to make it stand out from the surrounding text? (a boolean, defaults to :data:`True`) :param strip: Strip leading/trailing whitespace from the user's reply? :returns: The text entered by the user (a string) or the value of the `default` argument. :raises: - :exc:`~exceptions.KeyboardInterrupt` when the program is interrupted_ while the prompt is active, for example because the user presses Control-C_. - :exc:`~exceptions.EOFError` when reading from `standard input`_ fails, for example because the user presses Control-D_ or because the standard input stream is redirected (only if `default` is :data:`None`). .. _Control-C: https://en.wikipedia.org/wiki/Control-C#In_command-line_environments .. _Control-D: https://en.wikipedia.org/wiki/End-of-transmission_character#Meaning_in_Unix .. _interrupted: https://en.wikipedia.org/wiki/Unix_signal#SIGINT .. _standard input: https://en.wikipedia.org/wiki/Standard_streams#Standard_input_.28stdin.29 """ prepare_friendly_prompts() reply = None try: # Prefix an empty line to the text and indent by one space? if padding: question = '\n' + question question = question.replace('\n', '\n ') # Render the prompt and wait for the user's reply. try: reply = interactive_prompt(question) finally: if reply is None: # If the user terminated the prompt using Control-C or # Control-D instead of pressing Enter no newline will be # rendered after the prompt's text. The result looks kind of # weird: # # $ python -c 'print(raw_input("Are you sure? "))' # Are you sure? ^CTraceback (most recent call last): # File "", line 1, in # KeyboardInterrupt # # We can avoid this by emitting a newline ourselves if an # exception was raised (signaled by `reply' being None). sys.stderr.write('\n') if padding: # If the caller requested (didn't opt out of) `padding' then we'll # emit a newline regardless of whether an exception is being # handled. This helps to make interactive prompts `stand out' from # a surrounding `wall of text' on the terminal. sys.stderr.write('\n') except BaseException as e: if isinstance(e, EOFError) and default is not None: # If standard input isn't connected to an interactive terminal # but the caller provided a default we'll return that. logger.debug("Got EOF from terminal, returning default value (%r) ..", default) return default else: # Otherwise we log that the prompt was interrupted but propagate # the exception to the caller. logger.warning("Interactive prompt was interrupted by exception!", exc_info=True) raise if default is not None and not reply: # If the reply is empty and `default' is None we don't want to return # None because it's nicer for callers to be able to assume that the # return value is always a string. return default else: return reply.strip() def prepare_prompt_text(prompt_text, **options): """ Wrap a text to be rendered as an interactive prompt in ANSI escape sequences. :param prompt_text: The text to render on the prompt (a string). :param options: Any keyword arguments are passed on to :func:`.ansi_wrap()`. :returns: The resulting prompt text (a string). ANSI escape sequences are only used when the standard output stream is connected to a terminal. When the standard input stream is connected to a terminal any escape sequences are wrapped in "readline hints". """ return (ansi_wrap(prompt_text, readline_hints=connected_to_terminal(sys.stdin), **options) if terminal_supports_colors(sys.stdout) else prompt_text) def prepare_friendly_prompts(): u""" Make interactive prompts more user friendly. The prompts presented by :func:`python2:raw_input()` (in Python 2) and :func:`python3:input()` (in Python 3) are not very user friendly by default, for example the cursor keys (:kbd:`←`, :kbd:`↑`, :kbd:`→` and :kbd:`↓`) and the :kbd:`Home` and :kbd:`End` keys enter characters instead of performing the action you would expect them to. By simply importing the :mod:`readline` module these prompts become much friendlier (as mentioned in the Python standard library documentation). This function is called by the other functions in this module to enable user friendly prompts. """ import readline # NOQA def retry_limit(limit=MAX_ATTEMPTS): """ Allow the user to provide valid input up to `limit` times. :param limit: The maximum number of attempts (a number, defaults to :data:`MAX_ATTEMPTS`). :returns: A generator of numbers starting from one. :raises: :exc:`TooManyInvalidReplies` when an interactive prompt receives repeated invalid input (:data:`MAX_ATTEMPTS`). This function returns a generator for interactive prompts that want to repeat on invalid input without getting stuck in infinite loops. """ for i in range(limit): yield i + 1 msg = "Received too many invalid replies on interactive prompt, giving up! (tried %i times)" formatted_msg = msg % limit # Make sure the event is logged. logger.warning(formatted_msg) # Force the caller to decide what to do now. raise TooManyInvalidReplies(formatted_msg) class TooManyInvalidReplies(Exception): """Raised by interactive prompts when they've received too many invalid inputs.""" humanfriendly-4.18/humanfriendly/tables.py0000664000175000017500000003270613257003252021275 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: March 25, 2018 # URL: https://humanfriendly.readthedocs.io """ Functions that render ASCII tables. Some generic notes about the table formatting functions in this module: - These functions were not written with performance in mind (*at all*) because they're intended to format tabular data to be presented on a terminal. If someone were to run into a performance problem using these functions, they'd be printing so much tabular data to the terminal that a human wouldn't be able to digest the tabular data anyway, so the point is moot :-). - These functions ignore ANSI escape sequences (at least the ones generated by the :mod:`~humanfriendly.terminal` module) in the calculation of columns widths. On reason for this is that column names are highlighted in color when connected to a terminal. It also means that you can use ANSI escape sequences to highlight certain column's values if you feel like it (for example to highlight deviations from the norm in an overview of calculated values). """ # Standard library modules. import collections import re # Modules included in our package. from humanfriendly.compat import coerce_string from humanfriendly.terminal import ( ansi_strip, ansi_width, ansi_wrap, terminal_supports_colors, find_terminal_size, HIGHLIGHT_COLOR, ) # Public identifiers that require documentation. __all__ = ( 'format_pretty_table', 'format_robust_table', 'format_smart_table', ) # Compiled regular expression pattern to recognize table columns containing # numeric data (integer and/or floating point numbers). Used to right-align the # contents of such columns. # # Pre-emptive snarky comment: This pattern doesn't match every possible # floating point number notation!?!1!1 # # Response: I know, that's intentional. The use of this regular expression # pattern has a very high DWIM level and weird floating point notations do not # fall under the DWIM umbrella :-). NUMERIC_DATA_PATTERN = re.compile(r'^\d+(\.\d+)?$') def format_smart_table(data, column_names): """ Render tabular data using the most appropriate representation. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) containing the rows of the table, where each row is an iterable containing the columns of the table (strings). :param column_names: An iterable of column names (strings). :returns: The rendered table (a string). If you want an easy way to render tabular data on a terminal in a human friendly format then this function is for you! It works as follows: - If the input data doesn't contain any line breaks the function :func:`format_pretty_table()` is used to render a pretty table. If the resulting table fits in the terminal without wrapping the rendered pretty table is returned. - If the input data does contain line breaks or if a pretty table would wrap (given the width of the terminal) then the function :func:`format_robust_table()` is used to render a more robust table that can deal with data containing line breaks and long text. """ # Normalize the input in case we fall back from a pretty table to a robust # table (in which case we'll definitely iterate the input more than once). data = [normalize_columns(r) for r in data] column_names = normalize_columns(column_names) # Make sure the input data doesn't contain any line breaks (because pretty # tables break horribly when a column's text contains a line break :-). if not any(any('\n' in c for c in r) for r in data): # Render a pretty table. pretty_table = format_pretty_table(data, column_names) # Check if the pretty table fits in the terminal. table_width = max(map(ansi_width, pretty_table.splitlines())) num_rows, num_columns = find_terminal_size() if table_width <= num_columns: # The pretty table fits in the terminal without wrapping! return pretty_table # Fall back to a robust table when a pretty table won't work. return format_robust_table(data, column_names) def format_pretty_table(data, column_names=None, horizontal_bar='-', vertical_bar='|'): """ Render a table using characters like dashes and vertical bars to emulate borders. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) containing the rows of the table, where each row is an iterable containing the columns of the table (strings). :param column_names: An iterable of column names (strings). :param horizontal_bar: The character used to represent a horizontal bar (a string). :param vertical_bar: The character used to represent a vertical bar (a string). :returns: The rendered table (a string). Here's an example: >>> from humanfriendly.tables import format_pretty_table >>> column_names = ['Version', 'Uploaded on', 'Downloads'] >>> humanfriendly_releases = [ ... ['1.23', '2015-05-25', '218'], ... ['1.23.1', '2015-05-26', '1354'], ... ['1.24', '2015-05-26', '223'], ... ['1.25', '2015-05-26', '4319'], ... ['1.25.1', '2015-06-02', '197'], ... ] >>> print(format_pretty_table(humanfriendly_releases, column_names)) ------------------------------------- | Version | Uploaded on | Downloads | ------------------------------------- | 1.23 | 2015-05-25 | 218 | | 1.23.1 | 2015-05-26 | 1354 | | 1.24 | 2015-05-26 | 223 | | 1.25 | 2015-05-26 | 4319 | | 1.25.1 | 2015-06-02 | 197 | ------------------------------------- Notes about the resulting table: - If a column contains numeric data (integer and/or floating point numbers) in all rows (ignoring column names of course) then the content of that column is right-aligned, as can be seen in the example above. The idea here is to make it easier to compare the numbers in different columns to each other. - The column names are highlighted in color so they stand out a bit more (see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what that looks like (my terminals are always set to white text on a black background): .. image:: images/pretty-table.png """ # Normalize the input because we'll have to iterate it more than once. data = [normalize_columns(r) for r in data] if column_names is not None: column_names = normalize_columns(column_names) if column_names: if terminal_supports_colors(): column_names = [highlight_column_name(n) for n in column_names] data.insert(0, column_names) # Calculate the maximum width of each column. widths = collections.defaultdict(int) numeric_data = collections.defaultdict(list) for row_index, row in enumerate(data): for column_index, column in enumerate(row): widths[column_index] = max(widths[column_index], ansi_width(column)) if not (column_names and row_index == 0): numeric_data[column_index].append(bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column)))) # Create a horizontal bar of dashes as a delimiter. line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1) # Start the table with a vertical bar. lines = [line_delimiter] # Format the rows and columns. for row_index, row in enumerate(data): line = [vertical_bar] for column_index, column in enumerate(row): padding = ' ' * (widths[column_index] - ansi_width(column)) if all(numeric_data[column_index]): line.append(' ' + padding + column + ' ') else: line.append(' ' + column + padding + ' ') line.append(vertical_bar) lines.append(u''.join(line)) if column_names and row_index == 0: lines.append(line_delimiter) # End the table with a vertical bar. lines.append(line_delimiter) # Join the lines, returning a single string. return u'\n'.join(lines) def format_robust_table(data, column_names): """ Render tabular data with one column per line (allowing columns with line breaks). :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) containing the rows of the table, where each row is an iterable containing the columns of the table (strings). :param column_names: An iterable of column names (strings). :returns: The rendered table (a string). Here's an example: >>> from humanfriendly.tables import format_robust_table >>> column_names = ['Version', 'Uploaded on', 'Downloads'] >>> humanfriendly_releases = [ ... ['1.23', '2015-05-25', '218'], ... ['1.23.1', '2015-05-26', '1354'], ... ['1.24', '2015-05-26', '223'], ... ['1.25', '2015-05-26', '4319'], ... ['1.25.1', '2015-06-02', '197'], ... ] >>> print(format_robust_table(humanfriendly_releases, column_names)) ----------------------- Version: 1.23 Uploaded on: 2015-05-25 Downloads: 218 ----------------------- Version: 1.23.1 Uploaded on: 2015-05-26 Downloads: 1354 ----------------------- Version: 1.24 Uploaded on: 2015-05-26 Downloads: 223 ----------------------- Version: 1.25 Uploaded on: 2015-05-26 Downloads: 4319 ----------------------- Version: 1.25.1 Uploaded on: 2015-06-02 Downloads: 197 ----------------------- The column names are highlighted in bold font and color so they stand out a bit more (see :data:`.HIGHLIGHT_COLOR`). """ blocks = [] column_names = ["%s:" % n for n in normalize_columns(column_names)] if terminal_supports_colors(): column_names = [highlight_column_name(n) for n in column_names] # Convert each row into one or more `name: value' lines (one per column) # and group each `row of lines' into a block (i.e. rows become blocks). for row in data: lines = [] for column_index, column_text in enumerate(normalize_columns(row)): stripped_column = column_text.strip() if '\n' not in stripped_column: # Columns without line breaks are formatted inline. lines.append("%s %s" % (column_names[column_index], stripped_column)) else: # Columns with line breaks could very well contain indented # lines, so we'll put the column name on a separate line. This # way any indentation remains intact, and it's easier to # copy/paste the text. lines.append(column_names[column_index]) lines.extend(column_text.rstrip().splitlines()) blocks.append(lines) # Calculate the width of the row delimiter. num_rows, num_columns = find_terminal_size() longest_line = max(max(map(ansi_width, lines)) for lines in blocks) delimiter = u"\n%s\n" % ('-' * min(longest_line, num_columns)) # Force a delimiter at the start and end of the table. blocks.insert(0, "") blocks.append("") # Embed the row delimiter between every two blocks. return delimiter.join(u"\n".join(b) for b in blocks).strip() def format_rst_table(data, column_names=None): """ Render a table in reStructuredText_ format. :param data: An iterable (e.g. a :func:`tuple` or :class:`list`) containing the rows of the table, where each row is an iterable containing the columns of the table (strings). :param column_names: An iterable of column names (strings). :returns: The rendered table (a string). Here's an example: >>> from humanfriendly.tables import format_rst_table >>> column_names = ['Version', 'Uploaded on', 'Downloads'] >>> humanfriendly_releases = [ ... ['1.23', '2015-05-25', '218'], ... ['1.23.1', '2015-05-26', '1354'], ... ['1.24', '2015-05-26', '223'], ... ['1.25', '2015-05-26', '4319'], ... ['1.25.1', '2015-06-02', '197'], ... ] >>> print(format_rst_table(humanfriendly_releases, column_names)) ======= =========== ========= Version Uploaded on Downloads ======= =========== ========= 1.23 2015-05-25 218 1.23.1 2015-05-26 1354 1.24 2015-05-26 223 1.25 2015-05-26 4319 1.25.1 2015-06-02 197 ======= =========== ========= .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText """ data = [normalize_columns(r) for r in data] if column_names: data.insert(0, normalize_columns(column_names)) # Calculate the maximum width of each column. widths = collections.defaultdict(int) for row in data: for index, column in enumerate(row): widths[index] = max(widths[index], len(column)) # Pad the columns using whitespace. for row in data: for index, column in enumerate(row): if index < (len(row) - 1): row[index] = column.ljust(widths[index]) # Add table markers. delimiter = ['=' * w for i, w in sorted(widths.items())] if column_names: data.insert(1, delimiter) data.insert(0, delimiter) data.append(delimiter) # Join the lines and columns together. return '\n'.join(' '.join(r) for r in data) def normalize_columns(row): return [coerce_string(c) for c in row] def highlight_column_name(name): return ansi_wrap(name, bold=True, color=HIGHLIGHT_COLOR) humanfriendly-4.18/humanfriendly/sphinx.py0000664000175000017500000001246513270136377021346 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: February 17, 2016 # URL: https://humanfriendly.readthedocs.io """ Customizations for and integration with the Sphinx_ documentation generator. The :mod:`humanfriendly.sphinx` module uses the `Sphinx extension API`_ to customize the process of generating Sphinx based Python documentation. The most relevant functions to take a look at are :func:`setup()`, :func:`enable_special_methods()` and :func:`enable_usage_formatting()`. .. _Sphinx: http://www.sphinx-doc.org/ .. _Sphinx extension API: http://sphinx-doc.org/extdev/appapi.html """ # Standard library modules. import logging import types # Modules included in our package. from humanfriendly.usage import USAGE_MARKER, render_usage # Initialize a logger for this module. logger = logging.getLogger(__name__) def setup(app): """ Enable all of the provided Sphinx_ customizations. :param app: The Sphinx application object. The :func:`setup()` function makes it easy to enable all of the Sphinx customizations provided by the :mod:`humanfriendly.sphinx` module with the least amount of code. All you need to do is to add the module name to the ``extensions`` variable in your ``conf.py`` file: .. code-block:: python # Sphinx extension module names. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'humanfriendly.sphinx', ] When Sphinx sees the :mod:`humanfriendly.sphinx` name it will import the module and call its :func:`setup()` function. At the time of writing this just calls :func:`enable_special_methods()` and :func:`enable_usage_formatting()`, but of course more functionality may be added at a later stage. If you don't like that idea you may be better of calling the individual functions from your own ``setup()`` function. """ enable_special_methods(app) enable_usage_formatting(app) def enable_special_methods(app): """ Enable documenting "special methods" using the autodoc_ extension. :param app: The Sphinx application object. This function connects the :func:`special_methods_callback()` function to ``autodoc-skip-member`` events. .. _autodoc: http://www.sphinx-doc.org/en/stable/ext/autodoc.html """ app.connect('autodoc-skip-member', special_methods_callback) def special_methods_callback(app, what, name, obj, skip, options): """ Enable documenting "special methods" using the autodoc_ extension. Refer to :func:`enable_special_methods()` to enable the use of this function (you probably don't want to call :func:`special_methods_callback()` directly). This function implements a callback for ``autodoc-skip-member`` events to include documented "special methods" (method names with two leading and two trailing underscores) in your documentation. The result is similar to the use of the ``special-members`` flag with one big difference: Special methods are included but other types of members are ignored. This means that attributes like ``__weakref__`` will always be ignored (this was my main annoyance with the ``special-members`` flag). The parameters expected by this function are those defined for Sphinx event callback functions (i.e. I'm not going to document them here :-). """ if getattr(obj, '__doc__', None) and isinstance(obj, (types.FunctionType, types.MethodType)): return False else: return skip def enable_usage_formatting(app): """ Reformat human friendly usage messages to reStructuredText_. :param app: The Sphinx application object (as given to ``setup()``). This function connects the :func:`usage_message_callback()` function to ``autodoc-process-docstring`` events. .. _reStructuredText: https://en.wikipedia.org/wiki/ReStructuredText """ app.connect('autodoc-process-docstring', usage_message_callback) def usage_message_callback(app, what, name, obj, options, lines): """ Reformat human friendly usage messages to reStructuredText_. Refer to :func:`enable_usage_formatting()` to enable the use of this function (you probably don't want to call :func:`usage_message_callback()` directly). This function implements a callback for ``autodoc-process-docstring`` that reformats module docstrings using :func:`.render_usage()` so that Sphinx doesn't mangle usage messages that were written to be human readable instead of machine readable. Only module docstrings whose first line starts with :data:`.USAGE_MARKER` are reformatted. The parameters expected by this function are those defined for Sphinx event callback functions (i.e. I'm not going to document them here :-). """ # Make sure we only modify the docstrings of modules. if isinstance(obj, types.ModuleType) and lines: # Make sure we only modify docstrings containing a usage message. if lines[0].startswith(USAGE_MARKER): # Convert the usage message to reStructuredText. text = render_usage('\n'.join(lines)) # Clear the existing line buffer. while lines: lines.pop() # Fill up the buffer with our modified docstring. lines.extend(text.splitlines()) humanfriendly-4.18/humanfriendly/decorators.py0000644000175000017500000000267213362535475022203 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: October 20, 2018 # URL: https://humanfriendly.readthedocs.io """Simple function decorators to make Python programming easier.""" # Public identifiers that require documentation. __all__ = ('RESULTS_ATTRIBUTE', 'cached') RESULTS_ATTRIBUTE = 'cached_results' """The name of the property used to cache the return values of functions (a string).""" def cached(function): """ Rudimentary caching decorator for functions. :param function: The function whose return value should be cached. :returns: The decorated function. The given function will only be called once, the first time the wrapper function is called. The return value is cached by the wrapper function as an attribute of the given function and returned on each subsequent call. .. note:: Currently no function arguments are supported because only a single return value can be cached. Accepting any function arguments at all would imply that the cache is parametrized on function arguments, which is not currently the case. """ def wrapper(): try: return getattr(wrapper, RESULTS_ATTRIBUTE) except AttributeError: result = function() setattr(wrapper, RESULTS_ATTRIBUTE, result) return result wrapper.__doc__ = function.__doc__ return wrapper humanfriendly-4.18/humanfriendly/__init__.py0000644000175000017500000011754013433604150021560 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: February 21, 2019 # URL: https://humanfriendly.readthedocs.io """The main module of the `humanfriendly` package.""" # Standard library modules. import collections import datetime import decimal import multiprocessing import numbers import os import os.path import re import sys import time # In humanfriendly 1.23 the format_table() function was added to render a table # using characters like dashes and vertical bars to emulate borders. Since then # support for other tables has been added and the name of format_table() has # changed. The following import statement preserves backwards compatibility. from humanfriendly.tables import format_pretty_table as format_table # NOQA # In humanfriendly 1.30 the following text manipulation functions were moved # out into a separate module to enable their usage in other modules of the # humanfriendly package (without causing circular imports). from humanfriendly.text import ( # NOQA compact, concatenate, dedent, format, is_empty_line, pluralize, tokenize, trim_empty_lines, ) # In humanfriendly 1.38 the prompt_for_choice() function was moved out into a # separate module because several variants of interactive prompts were added. from humanfriendly.prompts import prompt_for_choice # NOQA # Compatibility with Python 2 and 3. from humanfriendly.compat import is_string, monotonic # Semi-standard module versioning. __version__ = '4.18' # Spinners are redrawn at most this many seconds. minimum_spinner_interval = 0.2 # The following ANSI escape sequence can be used to clear a line and move the # cursor back to the start of the line. erase_line_code = '\r\x1b[K' # ANSI escape sequences to hide and show the text cursor. hide_cursor_code = '\x1b[?25l' show_cursor_code = '\x1b[?25h' # Named tuples to define units of size. SizeUnit = collections.namedtuple('SizeUnit', 'divider, symbol, name') CombinedUnit = collections.namedtuple('CombinedUnit', 'decimal, binary') # Common disk size units in binary (base-2) and decimal (base-10) multiples. disk_size_units = ( CombinedUnit(SizeUnit(1000**1, 'KB', 'kilobyte'), SizeUnit(1024**1, 'KiB', 'kibibyte')), CombinedUnit(SizeUnit(1000**2, 'MB', 'megabyte'), SizeUnit(1024**2, 'MiB', 'mebibyte')), CombinedUnit(SizeUnit(1000**3, 'GB', 'gigabyte'), SizeUnit(1024**3, 'GiB', 'gibibyte')), CombinedUnit(SizeUnit(1000**4, 'TB', 'terabyte'), SizeUnit(1024**4, 'TiB', 'tebibyte')), CombinedUnit(SizeUnit(1000**5, 'PB', 'petabyte'), SizeUnit(1024**5, 'PiB', 'pebibyte')), CombinedUnit(SizeUnit(1000**6, 'EB', 'exabyte'), SizeUnit(1024**6, 'EiB', 'exbibyte')), CombinedUnit(SizeUnit(1000**7, 'ZB', 'zettabyte'), SizeUnit(1024**7, 'ZiB', 'zebibyte')), CombinedUnit(SizeUnit(1000**8, 'YB', 'yottabyte'), SizeUnit(1024**8, 'YiB', 'yobibyte')), ) # Common length size units, used for formatting and parsing. length_size_units = (dict(prefix='nm', divider=1e-09, singular='nm', plural='nm'), dict(prefix='mm', divider=1e-03, singular='mm', plural='mm'), dict(prefix='cm', divider=1e-02, singular='cm', plural='cm'), dict(prefix='m', divider=1, singular='metre', plural='metres'), dict(prefix='km', divider=1000, singular='km', plural='km')) # Common time units, used for formatting of time spans. time_units = (dict(divider=1e-3, singular='millisecond', plural='milliseconds', abbreviations=['ms']), dict(divider=1, singular='second', plural='seconds', abbreviations=['s', 'sec', 'secs']), dict(divider=60, singular='minute', plural='minutes', abbreviations=['m', 'min', 'mins']), dict(divider=60 * 60, singular='hour', plural='hours', abbreviations=['h']), dict(divider=60 * 60 * 24, singular='day', plural='days', abbreviations=['d']), dict(divider=60 * 60 * 24 * 7, singular='week', plural='weeks', abbreviations=['w']), dict(divider=60 * 60 * 24 * 7 * 52, singular='year', plural='years', abbreviations=['y'])) def coerce_boolean(value): """ Coerce any value to a boolean. :param value: Any Python value. If the value is a string: - The strings '1', 'yes', 'true' and 'on' are coerced to :data:`True`. - The strings '0', 'no', 'false' and 'off' are coerced to :data:`False`. - Other strings raise an exception. Other Python values are coerced using :func:`bool()`. :returns: A proper boolean value. :raises: :exc:`exceptions.ValueError` when the value is a string but cannot be coerced with certainty. """ if is_string(value): normalized = value.strip().lower() if normalized in ('1', 'yes', 'true', 'on'): return True elif normalized in ('0', 'no', 'false', 'off', ''): return False else: msg = "Failed to coerce string to boolean! (%r)" raise ValueError(format(msg, value)) else: return bool(value) def coerce_pattern(value, flags=0): """ Coerce strings to compiled regular expressions. :param value: A string containing a regular expression pattern or a compiled regular expression. :param flags: The flags used to compile the pattern (an integer). :returns: A compiled regular expression. :raises: :exc:`~exceptions.ValueError` when `value` isn't a string and also isn't a compiled regular expression. """ if is_string(value): value = re.compile(value, flags) else: empty_pattern = re.compile('') pattern_type = type(empty_pattern) if not isinstance(value, pattern_type): msg = "Failed to coerce value to compiled regular expression! (%r)" raise ValueError(format(msg, value)) return value def coerce_seconds(value): """ Coerce a value to the number of seconds. :param value: An :class:`int`, :class:`float` or :class:`datetime.timedelta` object. :returns: An :class:`int` or :class:`float` value. When `value` is a :class:`datetime.timedelta` object the :func:`~datetime.timedelta.total_seconds()` method is called. On Python 2.6 this method is not available so it is emulated. """ if isinstance(value, datetime.timedelta): if hasattr(value, 'total_seconds'): return value.total_seconds() else: return (value.microseconds + (value.seconds + value.days * 24 * 3600) * 10**6) / 10**6 if not isinstance(value, numbers.Number): msg = "Failed to coerce value to number of seconds! (%r)" raise ValueError(format(msg, value)) return value def format_size(num_bytes, keep_width=False, binary=False): """ Format a byte count as a human readable file size. :param num_bytes: The size to format in bytes (an integer). :param keep_width: :data:`True` if trailing zeros should not be stripped, :data:`False` if they can be stripped. :param binary: :data:`True` to use binary multiples of bytes (base-2), :data:`False` to use decimal multiples of bytes (base-10). :returns: The corresponding human readable file size (a string). This function knows how to format sizes in bytes, kilobytes, megabytes, gigabytes, terabytes and petabytes. Some examples: >>> from humanfriendly import format_size >>> format_size(0) '0 bytes' >>> format_size(1) '1 byte' >>> format_size(5) '5 bytes' > format_size(1000) '1 KB' > format_size(1024, binary=True) '1 KiB' >>> format_size(1000 ** 3 * 4) '4 GB' """ for unit in reversed(disk_size_units): if num_bytes >= unit.binary.divider and binary: number = round_number(float(num_bytes) / unit.binary.divider, keep_width=keep_width) return pluralize(number, unit.binary.symbol, unit.binary.symbol) elif num_bytes >= unit.decimal.divider and not binary: number = round_number(float(num_bytes) / unit.decimal.divider, keep_width=keep_width) return pluralize(number, unit.decimal.symbol, unit.decimal.symbol) return pluralize(num_bytes, 'byte') def parse_size(size, binary=False): """ Parse a human readable data size and return the number of bytes. :param size: The human readable file size to parse (a string). :param binary: :data:`True` to use binary multiples of bytes (base-2) for ambiguous unit symbols and names, :data:`False` to use decimal multiples of bytes (base-10). :returns: The corresponding size in bytes (an integer). :raises: :exc:`InvalidSize` when the input can't be parsed. This function knows how to parse sizes in bytes, kilobytes, megabytes, gigabytes, terabytes and petabytes. Some examples: >>> from humanfriendly import parse_size >>> parse_size('42') 42 >>> parse_size('13b') 13 >>> parse_size('5 bytes') 5 >>> parse_size('1 KB') 1000 >>> parse_size('1 kilobyte') 1000 >>> parse_size('1 KiB') 1024 >>> parse_size('1 KB', binary=True) 1024 >>> parse_size('1.5 GB') 1500000000 >>> parse_size('1.5 GB', binary=True) 1610612736 """ tokens = tokenize(size) if tokens and isinstance(tokens[0], numbers.Number): # Get the normalized unit (if any) from the tokenized input. normalized_unit = tokens[1].lower() if len(tokens) == 2 and is_string(tokens[1]) else '' # If the input contains only a number, it's assumed to be the number of # bytes. The second token can also explicitly reference the unit bytes. if len(tokens) == 1 or normalized_unit.startswith('b'): return int(tokens[0]) # Otherwise we expect two tokens: A number and a unit. if normalized_unit: for unit in disk_size_units: # First we check for unambiguous symbols (KiB, MiB, GiB, etc) # and names (kibibyte, mebibyte, gibibyte, etc) because their # handling is always the same. if normalized_unit in (unit.binary.symbol.lower(), unit.binary.name.lower()): return int(tokens[0] * unit.binary.divider) # Now we will deal with ambiguous prefixes (K, M, G, etc), # symbols (KB, MB, GB, etc) and names (kilobyte, megabyte, # gigabyte, etc) according to the caller's preference. if (normalized_unit in (unit.decimal.symbol.lower(), unit.decimal.name.lower()) or normalized_unit.startswith(unit.decimal.symbol[0].lower())): return int(tokens[0] * (unit.binary.divider if binary else unit.decimal.divider)) # We failed to parse the size specification. msg = "Failed to parse size! (input %r was tokenized as %r)" raise InvalidSize(format(msg, size, tokens)) def format_length(num_metres, keep_width=False): """ Format a metre count as a human readable length. :param num_metres: The length to format in metres (float / integer). :param keep_width: :data:`True` if trailing zeros should not be stripped, :data:`False` if they can be stripped. :returns: The corresponding human readable length (a string). This function supports ranges from nanometres to kilometres. Some examples: >>> from humanfriendly import format_length >>> format_length(0) '0 metres' >>> format_length(1) '1 metre' >>> format_length(5) '5 metres' >>> format_length(1000) '1 km' >>> format_length(0.004) '4 mm' """ for unit in reversed(length_size_units): if num_metres >= unit['divider']: number = round_number(float(num_metres) / unit['divider'], keep_width=keep_width) return pluralize(number, unit['singular'], unit['plural']) return pluralize(num_metres, 'metre') def parse_length(length): """ Parse a human readable length and return the number of metres. :param length: The human readable length to parse (a string). :returns: The corresponding length in metres (a float). :raises: :exc:`InvalidLength` when the input can't be parsed. Some examples: >>> from humanfriendly import parse_length >>> parse_length('42') 42 >>> parse_length('1 km') 1000 >>> parse_length('5mm') 0.005 >>> parse_length('15.3cm') 0.153 """ tokens = tokenize(length) if tokens and isinstance(tokens[0], numbers.Number): # If the input contains only a number, it's assumed to be the number of metres. if len(tokens) == 1: return int(tokens[0]) # Otherwise we expect to find two tokens: A number and a unit. if len(tokens) == 2 and is_string(tokens[1]): normalized_unit = tokens[1].lower() # Try to match the first letter of the unit. for unit in length_size_units: if normalized_unit.startswith(unit['prefix']): return tokens[0] * unit['divider'] # We failed to parse the length specification. msg = "Failed to parse length! (input %r was tokenized as %r)" raise InvalidLength(format(msg, length, tokens)) def format_number(number, num_decimals=2): """ Format a number as a string including thousands separators. :param number: The number to format (a number like an :class:`int`, :class:`long` or :class:`float`). :param num_decimals: The number of decimals to render (2 by default). If no decimal places are required to represent the number they will be omitted regardless of this argument. :returns: The formatted number (a string). This function is intended to make it easier to recognize the order of size of the number being formatted. Here's an example: >>> from humanfriendly import format_number >>> print(format_number(6000000)) 6,000,000 > print(format_number(6000000000.42)) 6,000,000,000.42 > print(format_number(6000000000.42, num_decimals=0)) 6,000,000,000 """ integer_part, _, decimal_part = str(float(number)).partition('.') reversed_digits = ''.join(reversed(integer_part)) parts = [] while reversed_digits: parts.append(reversed_digits[:3]) reversed_digits = reversed_digits[3:] formatted_number = ''.join(reversed(','.join(parts))) decimals_to_add = decimal_part[:num_decimals].rstrip('0') if decimals_to_add: formatted_number += '.' + decimals_to_add return formatted_number def round_number(count, keep_width=False): """ Round a floating point number to two decimal places in a human friendly format. :param count: The number to format. :param keep_width: :data:`True` if trailing zeros should not be stripped, :data:`False` if they can be stripped. :returns: The formatted number as a string. If no decimal places are required to represent the number, they will be omitted. The main purpose of this function is to be used by functions like :func:`format_length()`, :func:`format_size()` and :func:`format_timespan()`. Here are some examples: >>> from humanfriendly import round_number >>> round_number(1) '1' >>> round_number(math.pi) '3.14' >>> round_number(5.001) '5' """ text = '%.2f' % float(count) if not keep_width: text = re.sub('0+$', '', text) text = re.sub(r'\.$', '', text) return text def format_timespan(num_seconds, detailed=False, max_units=3): """ Format a timespan in seconds as a human readable string. :param num_seconds: Any value accepted by :func:`coerce_seconds()`. :param detailed: If :data:`True` milliseconds are represented separately instead of being represented as fractional seconds (defaults to :data:`False`). :param max_units: The maximum number of units to show in the formatted time span (an integer, defaults to three). :returns: The formatted timespan as a string. :raise: See :func:`coerce_seconds()`. Some examples: >>> from humanfriendly import format_timespan >>> format_timespan(0) '0 seconds' >>> format_timespan(1) '1 second' >>> import math >>> format_timespan(math.pi) '3.14 seconds' >>> hour = 60 * 60 >>> day = hour * 24 >>> week = day * 7 >>> format_timespan(week * 52 + day * 2 + hour * 3) '1 year, 2 days and 3 hours' """ num_seconds = coerce_seconds(num_seconds) if num_seconds < 60 and not detailed: # Fast path. return pluralize(round_number(num_seconds), 'second') else: # Slow path. result = [] num_seconds = decimal.Decimal(str(num_seconds)) relevant_units = list(reversed(time_units[0 if detailed else 1:])) for unit in relevant_units: # Extract the unit count from the remaining time. divider = decimal.Decimal(str(unit['divider'])) count = num_seconds / divider num_seconds %= divider # Round the unit count appropriately. if unit != relevant_units[-1]: # Integer rounding for all but the smallest unit. count = int(count) else: # Floating point rounding for the smallest unit. count = round_number(count) # Only include relevant units in the result. if count not in (0, '0'): result.append(pluralize(count, unit['singular'], unit['plural'])) if len(result) == 1: # A single count/unit combination. return result[0] else: if not detailed: # Remove `insignificant' data from the formatted timespan. result = result[:max_units] # Format the timespan in a readable way. return concatenate(result) def parse_timespan(timespan): """ Parse a "human friendly" timespan into the number of seconds. :param value: A string like ``5h`` (5 hours), ``10m`` (10 minutes) or ``42s`` (42 seconds). :returns: The number of seconds as a floating point number. :raises: :exc:`InvalidTimespan` when the input can't be parsed. Note that the :func:`parse_timespan()` function is not meant to be the "mirror image" of the :func:`format_timespan()` function. Instead it's meant to allow humans to easily and succinctly specify a timespan with a minimal amount of typing. It's very useful to accept easy to write time spans as e.g. command line arguments to programs. The time units (and abbreviations) supported by this function are: - ms, millisecond, milliseconds - s, sec, secs, second, seconds - m, min, mins, minute, minutes - h, hour, hours - d, day, days - w, week, weeks - y, year, years Some examples: >>> from humanfriendly import parse_timespan >>> parse_timespan('42') 42.0 >>> parse_timespan('42s') 42.0 >>> parse_timespan('1m') 60.0 >>> parse_timespan('1h') 3600.0 >>> parse_timespan('1d') 86400.0 """ tokens = tokenize(timespan) if tokens and isinstance(tokens[0], numbers.Number): # If the input contains only a number, it's assumed to be the number of seconds. if len(tokens) == 1: return float(tokens[0]) # Otherwise we expect to find two tokens: A number and a unit. if len(tokens) == 2 and is_string(tokens[1]): normalized_unit = tokens[1].lower() for unit in time_units: if (normalized_unit == unit['singular'] or normalized_unit == unit['plural'] or normalized_unit in unit['abbreviations']): return float(tokens[0]) * unit['divider'] # We failed to parse the timespan specification. msg = "Failed to parse timespan! (input %r was tokenized as %r)" raise InvalidTimespan(format(msg, timespan, tokens)) def parse_date(datestring): """ Parse a date/time string into a tuple of integers. :param datestring: The date/time string to parse. :returns: A tuple with the numbers ``(year, month, day, hour, minute, second)`` (all numbers are integers). :raises: :exc:`InvalidDate` when the date cannot be parsed. Supported date/time formats: - ``YYYY-MM-DD`` - ``YYYY-MM-DD HH:MM:SS`` .. note:: If you want to parse date/time strings with a fixed, known format and :func:`parse_date()` isn't useful to you, consider :func:`time.strptime()` or :meth:`datetime.datetime.strptime()`, both of which are included in the Python standard library. Alternatively for more complex tasks consider using the date/time parsing module in the dateutil_ package. Examples: >>> from humanfriendly import parse_date >>> parse_date('2013-06-17') (2013, 6, 17, 0, 0, 0) >>> parse_date('2013-06-17 02:47:42') (2013, 6, 17, 2, 47, 42) Here's how you convert the result to a number (`Unix time`_): >>> from humanfriendly import parse_date >>> from time import mktime >>> mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1)) 1371430062.0 And here's how you convert it to a :class:`datetime.datetime` object: >>> from humanfriendly import parse_date >>> from datetime import datetime >>> datetime(*parse_date('2013-06-17 02:47:42')) datetime.datetime(2013, 6, 17, 2, 47, 42) Here's an example that combines :func:`format_timespan()` and :func:`parse_date()` to calculate a human friendly timespan since a given date: >>> from humanfriendly import format_timespan, parse_date >>> from time import mktime, time >>> unix_time = mktime(parse_date('2013-06-17 02:47:42') + (-1, -1, -1)) >>> seconds_since_then = time() - unix_time >>> print(format_timespan(seconds_since_then)) 1 year, 43 weeks and 1 day .. _dateutil: https://dateutil.readthedocs.io/en/latest/parser.html .. _Unix time: http://en.wikipedia.org/wiki/Unix_time """ try: tokens = [t.strip() for t in datestring.split()] if len(tokens) >= 2: date_parts = list(map(int, tokens[0].split('-'))) + [1, 1] time_parts = list(map(int, tokens[1].split(':'))) + [0, 0, 0] return tuple(date_parts[0:3] + time_parts[0:3]) else: year, month, day = (list(map(int, datestring.split('-'))) + [1, 1])[0:3] return (year, month, day, 0, 0, 0) except Exception: msg = "Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: %r)" raise InvalidDate(format(msg, datestring)) def format_path(pathname): """ Shorten a pathname to make it more human friendly. :param pathname: An absolute pathname (a string). :returns: The pathname with the user's home directory abbreviated. Given an absolute pathname, this function abbreviates the user's home directory to ``~/`` in order to shorten the pathname without losing information. It is not an error if the pathname is not relative to the current user's home directory. Here's an example of its usage: >>> from os import environ >>> from os.path import join >>> vimrc = join(environ['HOME'], '.vimrc') >>> vimrc '/home/peter/.vimrc' >>> from humanfriendly import format_path >>> format_path(vimrc) '~/.vimrc' """ pathname = os.path.abspath(pathname) home = os.environ.get('HOME') if home: home = os.path.abspath(home) if pathname.startswith(home): pathname = os.path.join('~', os.path.relpath(pathname, home)) return pathname def parse_path(pathname): """ Convert a human friendly pathname to an absolute pathname. Expands leading tildes using :func:`os.path.expanduser()` and environment variables using :func:`os.path.expandvars()` and makes the resulting pathname absolute using :func:`os.path.abspath()`. :param pathname: A human friendly pathname (a string). :returns: An absolute pathname (a string). """ return os.path.abspath(os.path.expanduser(os.path.expandvars(pathname))) class Timer(object): """ Easy to use timer to keep track of long during operations. """ def __init__(self, start_time=None, resumable=False): """ Remember the time when the :class:`Timer` was created. :param start_time: The start time (a float, defaults to the current time). :param resumable: Create a resumable timer (defaults to :data:`False`). When `start_time` is given :class:`Timer` uses :func:`time.time()` as a clock source, otherwise it uses :func:`humanfriendly.compat.monotonic()`. """ if resumable: self.monotonic = True self.resumable = True self.start_time = 0.0 self.total_time = 0.0 elif start_time: self.monotonic = False self.resumable = False self.start_time = start_time else: self.monotonic = True self.resumable = False self.start_time = monotonic() def __enter__(self): """ Start or resume counting elapsed time. :returns: The :class:`Timer` object. :raises: :exc:`~exceptions.ValueError` when the timer isn't resumable. """ if not self.resumable: raise ValueError("Timer is not resumable!") self.start_time = monotonic() return self def __exit__(self, exc_type=None, exc_value=None, traceback=None): """ Stop counting elapsed time. :raises: :exc:`~exceptions.ValueError` when the timer isn't resumable. """ if not self.resumable: raise ValueError("Timer is not resumable!") if self.start_time: self.total_time += monotonic() - self.start_time self.start_time = 0.0 def sleep(self, seconds): """ Easy to use rate limiting of repeating actions. :param seconds: The number of seconds to sleep (an integer or floating point number). This method sleeps for the given number of seconds minus the :attr:`elapsed_time`. If the resulting duration is negative :func:`time.sleep()` will still be called, but the argument given to it will be the number 0 (negative numbers cause :func:`time.sleep()` to raise an exception). The use case for this is to initialize a :class:`Timer` inside the body of a :keyword:`for` or :keyword:`while` loop and call :func:`Timer.sleep()` at the end of the loop body to rate limit whatever it is that is being done inside the loop body. For posterity: Although the implementation of :func:`sleep()` only requires a single line of code I've added it to :mod:`humanfriendly` anyway because now that I've thought about how to tackle this once I never want to have to think about it again :-P (unless I find ways to improve this). """ time.sleep(max(0, seconds - self.elapsed_time)) @property def elapsed_time(self): """ Get the number of seconds counted so far. """ elapsed_time = 0 if self.resumable: elapsed_time += self.total_time if self.start_time: current_time = monotonic() if self.monotonic else time.time() elapsed_time += current_time - self.start_time return elapsed_time @property def rounded(self): """Human readable timespan rounded to seconds (a string).""" return format_timespan(round(self.elapsed_time)) def __str__(self): """Show the elapsed time since the :class:`Timer` was created.""" return format_timespan(self.elapsed_time) class Spinner(object): """ Show a spinner on the terminal as a simple means of feedback to the user. The :class:`Spinner` class shows a "spinner" on the terminal to let the user know that something is happening during long running operations that would otherwise be silent (leaving the user to wonder what they're waiting for). Below are some visual examples that should illustrate the point. **Simple spinners:** Here's a screen capture that shows the simplest form of spinner: .. image:: images/spinner-basic.gif :alt: Animated screen capture of a simple spinner. The following code was used to create the spinner above: .. code-block:: python import itertools import time from humanfriendly import Spinner with Spinner(label="Downloading") as spinner: for i in itertools.count(): # Do something useful here. time.sleep(0.1) # Advance the spinner. spinner.step() **Spinners that show elapsed time:** Here's a spinner that shows the elapsed time since it started: .. image:: images/spinner-with-timer.gif :alt: Animated screen capture of a spinner showing elapsed time. The following code was used to create the spinner above: .. code-block:: python import itertools import time from humanfriendly import Spinner, Timer with Spinner(label="Downloading", timer=Timer()) as spinner: for i in itertools.count(): # Do something useful here. time.sleep(0.1) # Advance the spinner. spinner.step() **Spinners that show progress:** Here's a spinner that shows a progress percentage: .. image:: images/spinner-with-progress.gif :alt: Animated screen capture of spinner showing progress. The following code was used to create the spinner above: .. code-block:: python import itertools import random import time from humanfriendly import Spinner, Timer with Spinner(label="Downloading", total=100) as spinner: progress = 0 while progress < 100: # Do something useful here. time.sleep(0.1) # Advance the spinner. spinner.step(progress) # Determine the new progress value. progress += random.random() * 5 If you want to provide user feedback during a long running operation but it's not practical to periodically call the :func:`~Spinner.step()` method consider using :class:`AutomaticSpinner` instead. As you may already have noticed in the examples above, :class:`Spinner` objects can be used as context managers to automatically call :func:`clear()` when the spinner ends. This helps to make sure that if the text cursor is hidden its visibility is restored before the spinner ends (even if an exception interrupts the spinner). """ def __init__(self, label=None, total=0, stream=sys.stderr, interactive=None, timer=None, hide_cursor=True): """ Initialize a spinner. :param label: The label for the spinner (a string, defaults to :data:`None`). :param total: The expected number of steps (an integer). :param stream: The output stream to show the spinner on (defaults to :data:`sys.stderr`). :param interactive: If this is :data:`False` then the spinner doesn't write to the output stream at all. It defaults to the return value of ``stream.isatty()``. :param timer: A :class:`Timer` object (optional). If this is given the spinner will show the elapsed time according to the timer. :param hide_cursor: If :data:`True` (the default) the text cursor is hidden as long as the spinner is active. """ self.label = label self.total = total self.stream = stream self.states = ['-', '\\', '|', '/'] self.counter = 0 self.last_update = 0 if interactive is None: # Try to automatically discover whether the stream is connected to # a terminal, but don't fail if no isatty() method is available. try: interactive = stream.isatty() except Exception: interactive = False self.interactive = interactive self.timer = timer self.hide_cursor = hide_cursor if self.interactive and self.hide_cursor: self.stream.write(hide_cursor_code) def step(self, progress=0, label=None): """ Advance the spinner by one step and redraw it. :param progress: The number of the current step, relative to the total given to the :class:`Spinner` constructor (an integer, optional). If not provided the spinner will not show progress. :param label: The label to use while redrawing (a string, optional). If not provided the label given to the :class:`Spinner` constructor is used instead. This method advances the spinner by one step without starting a new line, causing an animated effect which is very simple but much nicer than waiting for a prompt which is completely silent for a long time. .. note:: This method uses time based rate limiting to avoid redrawing the spinner too frequently. If you know you're dealing with code that will call :func:`step()` at a high frequency, consider using :func:`sleep()` to avoid creating the equivalent of a busy loop that's rate limiting the spinner 99% of the time. """ if self.interactive: time_now = time.time() if time_now - self.last_update >= minimum_spinner_interval: self.last_update = time_now state = self.states[self.counter % len(self.states)] label = label or self.label if not label: raise Exception("No label set for spinner!") elif self.total and progress: label = "%s: %.2f%%" % (label, progress / (self.total / 100.0)) elif self.timer and self.timer.elapsed_time > 2: label = "%s (%s)" % (label, self.timer.rounded) self.stream.write("%s %s %s ..\r" % (erase_line_code, state, label)) self.counter += 1 def sleep(self): """ Sleep for a short period before redrawing the spinner. This method is useful when you know you're dealing with code that will call :func:`step()` at a high frequency. It will sleep for the interval with which the spinner is redrawn (less than a second). This avoids creating the equivalent of a busy loop that's rate limiting the spinner 99% of the time. This method doesn't redraw the spinner, you still have to call :func:`step()` in order to do that. """ time.sleep(minimum_spinner_interval) def clear(self): """ Clear the spinner. The next line which is shown on the standard output or error stream after calling this method will overwrite the line that used to show the spinner. Also the visibility of the text cursor is restored. """ if self.interactive: if self.hide_cursor: self.stream.write(show_cursor_code) self.stream.write(erase_line_code) def __enter__(self): """ Enable the use of spinners as context managers. :returns: The :class:`Spinner` object. """ return self def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Clear the spinner when leaving the context.""" self.clear() class AutomaticSpinner(object): """ Show a spinner on the terminal that automatically starts animating. This class shows a spinner on the terminal (just like :class:`Spinner` does) that automatically starts animating. This class should be used as a context manager using the :keyword:`with` statement. The animation continues for as long as the context is active. :class:`AutomaticSpinner` provides an alternative to :class:`Spinner` for situations where it is not practical for the caller to periodically call :func:`~Spinner.step()` to advance the animation, e.g. because you're performing a blocking call and don't fancy implementing threading or subprocess handling just to provide some user feedback. This works using the :mod:`multiprocessing` module by spawning a subprocess to render the spinner while the main process is busy doing something more useful. By using the :keyword:`with` statement you're guaranteed that the subprocess is properly terminated at the appropriate time. """ def __init__(self, label, show_time=True): """ Initialize an automatic spinner. :param label: The label for the spinner (a string). :param show_time: If this is :data:`True` (the default) then the spinner shows elapsed time. """ self.label = label self.show_time = show_time self.shutdown_event = multiprocessing.Event() self.subprocess = multiprocessing.Process(target=self._target) def __enter__(self): """Enable the use of automatic spinners as context managers.""" self.subprocess.start() def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Enable the use of automatic spinners as context managers.""" self.shutdown_event.set() self.subprocess.join() def _target(self): try: timer = Timer() if self.show_time else None with Spinner(label=self.label, timer=timer) as spinner: while not self.shutdown_event.is_set(): spinner.step() spinner.sleep() except KeyboardInterrupt: # Swallow Control-C signals without producing a nasty traceback that # won't make any sense to the average user. pass class InvalidDate(Exception): """ Raised when a string cannot be parsed into a date. For example: >>> from humanfriendly import parse_date >>> parse_date('2013-06-XY') Traceback (most recent call last): File "humanfriendly.py", line 206, in parse_date raise InvalidDate(format(msg, datestring)) humanfriendly.InvalidDate: Invalid date! (expected 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS' but got: '2013-06-XY') """ class InvalidSize(Exception): """ Raised when a string cannot be parsed into a file size. For example: >>> from humanfriendly import parse_size >>> parse_size('5 Z') Traceback (most recent call last): File "humanfriendly/__init__.py", line 267, in parse_size raise InvalidSize(format(msg, size, tokens)) humanfriendly.InvalidSize: Failed to parse size! (input '5 Z' was tokenized as [5, 'Z']) """ class InvalidLength(Exception): """ Raised when a string cannot be parsed into a length. For example: >>> from humanfriendly import parse_length >>> parse_length('5 Z') Traceback (most recent call last): File "humanfriendly/__init__.py", line 267, in parse_length raise InvalidLength(format(msg, length, tokens)) humanfriendly.InvalidLength: Failed to parse length! (input '5 Z' was tokenized as [5, 'Z']) """ class InvalidTimespan(Exception): """ Raised when a string cannot be parsed into a timespan. For example: >>> from humanfriendly import parse_timespan >>> parse_timespan('1 age') Traceback (most recent call last): File "humanfriendly/__init__.py", line 419, in parse_timespan raise InvalidTimespan(format(msg, timespan, tokens)) humanfriendly.InvalidTimespan: Failed to parse timespan! (input '1 age' was tokenized as [1, 'age']) """ humanfriendly-4.18/humanfriendly/testing.py0000664000175000017500000006170613142151662021504 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: July 16, 2017 # URL: https://humanfriendly.readthedocs.io """ Utility classes and functions that make it easy to write :mod:`unittest` compatible test suites. Over the years I've developed the habit of writing test suites for Python projects using the :mod:`unittest` module. During those years I've come to know pytest_ and in fact I use pytest to run my test suites (due to its much better error reporting) but I've yet to publish a test suite that *requires* pytest. I have several reasons for doing so: - It's nice to keep my test suites as simple and accessible as possible and not requiring a specific test runner is part of that attitude. - Whereas :mod:`unittest` is quite explicit, pytest contains a lot of magic, which kind of contradicts the Python mantra "explicit is better than implicit" (IMHO). .. _pytest: https://docs.pytest.org """ # Standard library module import functools import logging import os import pipes import shutil import sys import tempfile import time # Modules included in our package. from humanfriendly.compat import StringIO, unicode, unittest from humanfriendly.text import compact, random_string # Initialize a logger for this module. logger = logging.getLogger(__name__) # A unique object reference used to detect missing attributes. NOTHING = object() # Public identifiers that require documentation. __all__ = ( 'CallableTimedOut', 'CaptureOutput', 'ContextManager', 'CustomSearchPath', 'MockedProgram', 'PatchedAttribute', 'PatchedItem', 'TemporaryDirectory', 'TestCase', 'configure_logging', 'make_dirs', 'retry', 'run_cli', 'touch', ) def configure_logging(log_level=logging.DEBUG): """configure_logging(log_level=logging.DEBUG) Automatically configure logging to the terminal. :param log_level: The log verbosity (a number, defaults to :data:`logging.DEBUG`). When :mod:`coloredlogs` is installed :func:`coloredlogs.install()` will be used to configure logging to the terminal. When this fails with an :exc:`~exceptions.ImportError` then :func:`logging.basicConfig()` is used as a fall back. """ try: import coloredlogs coloredlogs.install(level=log_level) except ImportError: logging.basicConfig( level=log_level, format='%(asctime)s %(name)s[%(process)d] %(levelname)s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') def make_dirs(pathname): """ Create missing directories. :param pathname: The pathname of a directory (a string). """ if not os.path.isdir(pathname): os.makedirs(pathname) def retry(func, timeout=60, exc_type=AssertionError): """retry(func, timeout=60, exc_type=AssertionError) Retry a function until assertions no longer fail. :param func: A callable. When the callable returns :data:`False` it will also be retried. :param timeout: The number of seconds after which to abort (a number, defaults to 60). :param exc_type: The type of exceptions to retry (defaults to :exc:`~exceptions.AssertionError`). :returns: The value returned by `func`. :raises: Once the timeout has expired :func:`retry()` will raise the previously retried assertion error. When `func` keeps returning :data:`False` until `timeout` expires :exc:`CallableTimedOut` will be raised. This function sleeps between retries to avoid claiming CPU cycles we don't need. It starts by sleeping for 0.1 second but adjusts this to one second as the number of retries grows. """ pause = 0.1 timeout += time.time() while True: try: result = func() if result is not False: return result except exc_type: if time.time() > timeout: raise else: if time.time() > timeout: raise CallableTimedOut() time.sleep(pause) if pause < 1: pause *= 2 def run_cli(entry_point, *arguments, **options): """ Test a command line entry point. :param entry_point: The function that implements the command line interface (a callable). :param arguments: Any positional arguments (strings) become the command line arguments (:data:`sys.argv` items 1-N). :param options: The following keyword arguments are supported: **input** Refer to :class:`CaptureOutput`. **merged** Refer to :class:`CaptureOutput`. **program_name** Used to set :data:`sys.argv` item 0. :returns: A tuple with two values: 1. The return code (an integer). 2. The captured output (a string). """ merged = options.get('merged', False) # Add the `program_name' option to the arguments. arguments = list(arguments) arguments.insert(0, options.pop('program_name', sys.executable)) # Log the command line arguments (and the fact that we're about to call the # command line entry point function). logger.debug("Calling command line entry point with arguments: %s", arguments) # Prepare to capture the return code and output even if the command line # interface raises an exception (whether the exception type is SystemExit # or something else). returncode = 0 stdout = None stderr = None try: # Temporarily override sys.argv. with PatchedAttribute(sys, 'argv', arguments): # Manipulate the standard input/output/error streams. with CaptureOutput(**options) as capturer: try: # Call the command line interface. entry_point() finally: # Get the output even if an exception is raised. stdout = capturer.stdout.getvalue() stderr = capturer.stderr.getvalue() # Reconfigure logging to the terminal because it is very # likely that the entry point function has changed the # configured log level. configure_logging() except BaseException as e: if isinstance(e, SystemExit): logger.debug("Intercepting return code %s from SystemExit exception.", e.code) returncode = e.code else: logger.warning("Defaulting return code to 1 due to raised exception.", exc_info=True) returncode = 1 else: logger.debug("Command line entry point returned successfully!") # Always log the output captured on stdout/stderr, to make it easier to # diagnose test failures (but avoid duplicate logging when merged=True). merged_streams = [('merged streams', stdout)] separate_streams = [('stdout', stdout), ('stderr', stderr)] streams = merged_streams if merged else separate_streams for name, value in streams: if value: logger.debug("Output on %s:\n%s", name, value) else: logger.debug("No output on %s.", name) return returncode, stdout def touch(filename): """ The equivalent of the UNIX ``touch`` program in Python. :param filename: The pathname of the file to touch (a string). Note that missing directories are automatically created using :func:`make_dirs()`. """ make_dirs(os.path.dirname(filename)) with open(filename, 'a'): os.utime(filename, None) class CallableTimedOut(Exception): """Raised by :func:`retry()` when the timeout expires.""" class ContextManager(object): """Base class to enable composition of context managers.""" def __enter__(self): """Enable use as context managers.""" return self def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Enable use as context managers.""" class PatchedAttribute(ContextManager): """Context manager that temporary replaces an object attribute using :func:`setattr()`.""" def __init__(self, obj, name, value): """ Initialize a :class:`PatchedAttribute` object. :param obj: The object to patch. :param name: An attribute name. :param value: The value to set. """ self.object_to_patch = obj self.attribute_to_patch = name self.patched_value = value self.original_value = NOTHING def __enter__(self): """ Replace (patch) the attribute. :returns: The object whose attribute was patched. """ # Enable composition of context managers. super(PatchedAttribute, self).__enter__() # Patch the object's attribute. self.original_value = getattr(self.object_to_patch, self.attribute_to_patch, NOTHING) setattr(self.object_to_patch, self.attribute_to_patch, self.patched_value) return self.object_to_patch def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Restore the attribute to its original value.""" # Enable composition of context managers. super(PatchedAttribute, self).__exit__(exc_type, exc_value, traceback) # Restore the object's attribute. if self.original_value is NOTHING: delattr(self.object_to_patch, self.attribute_to_patch) else: setattr(self.object_to_patch, self.attribute_to_patch, self.original_value) class PatchedItem(ContextManager): """Context manager that temporary replaces an object item using :func:`~object.__setitem__()`.""" def __init__(self, obj, item, value): """ Initialize a :class:`PatchedItem` object. :param obj: The object to patch. :param item: The item to patch. :param value: The value to set. """ self.object_to_patch = obj self.item_to_patch = item self.patched_value = value self.original_value = NOTHING def __enter__(self): """ Replace (patch) the item. :returns: The object whose item was patched. """ # Enable composition of context managers. super(PatchedItem, self).__enter__() # Patch the object's item. try: self.original_value = self.object_to_patch[self.item_to_patch] except KeyError: self.original_value = NOTHING self.object_to_patch[self.item_to_patch] = self.patched_value return self.object_to_patch def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Restore the item to its original value.""" # Enable composition of context managers. super(PatchedItem, self).__exit__(exc_type, exc_value, traceback) # Restore the object's item. if self.original_value is NOTHING: del self.object_to_patch[self.item_to_patch] else: self.object_to_patch[self.item_to_patch] = self.original_value class TemporaryDirectory(ContextManager): """ Easy temporary directory creation & cleanup using the :keyword:`with` statement. Here's an example of how to use this: .. code-block:: python with TemporaryDirectory() as directory: # Do something useful here. assert os.path.isdir(directory) """ def __init__(self, **options): """ Initialize a :class:`TemporaryDirectory` object. :param options: Any keyword arguments are passed on to :func:`tempfile.mkdtemp()`. """ self.mkdtemp_options = options self.temporary_directory = None def __enter__(self): """ Create the temporary directory using :func:`tempfile.mkdtemp()`. :returns: The pathname of the directory (a string). """ # Enable composition of context managers. super(TemporaryDirectory, self).__enter__() # Create the temporary directory. self.temporary_directory = tempfile.mkdtemp(**self.mkdtemp_options) return self.temporary_directory def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Cleanup the temporary directory using :func:`shutil.rmtree()`.""" # Enable composition of context managers. super(TemporaryDirectory, self).__exit__(exc_type, exc_value, traceback) # Cleanup the temporary directory. if self.temporary_directory is not None: shutil.rmtree(self.temporary_directory) self.temporary_directory = None class MockedHomeDirectory(PatchedItem, TemporaryDirectory): """ Context manager to temporarily change ``$HOME`` (the current user's profile directory). This class is a composition of the :class:`PatchedItem` and :class:`TemporaryDirectory` context managers. """ def __init__(self): """Initialize a :class:`MockedHomeDirectory` object.""" PatchedItem.__init__(self, os.environ, 'HOME', os.environ.get('HOME')) TemporaryDirectory.__init__(self) def __enter__(self): """ Activate the custom ``$PATH``. :returns: The pathname of the directory that has been added to ``$PATH`` (a string). """ # Get the temporary directory. directory = TemporaryDirectory.__enter__(self) # Override the value to patch now that we have # the pathname of the temporary directory. self.patched_value = directory # Temporary patch $HOME. PatchedItem.__enter__(self) # Pass the pathname of the temporary directory to the caller. return directory def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Deactivate the custom ``$HOME``.""" super(MockedHomeDirectory, self).__exit__(exc_type, exc_value, traceback) class CustomSearchPath(PatchedItem, TemporaryDirectory): """ Context manager to temporarily customize ``$PATH`` (the executable search path). This class is a composition of the :class:`PatchedItem` and :class:`TemporaryDirectory` context managers. """ def __init__(self, isolated=False): """ Initialize a :class:`CustomSearchPath` object. :param isolated: :data:`True` to clear the original search path, :data:`False` to add the temporary directory to the start of the search path. """ # Initialize our own instance variables. self.isolated_search_path = isolated # Selectively initialize our superclasses. PatchedItem.__init__(self, os.environ, 'PATH', self.current_search_path) TemporaryDirectory.__init__(self) def __enter__(self): """ Activate the custom ``$PATH``. :returns: The pathname of the directory that has been added to ``$PATH`` (a string). """ # Get the temporary directory. directory = TemporaryDirectory.__enter__(self) # Override the value to patch now that we have # the pathname of the temporary directory. self.patched_value = ( directory if self.isolated_search_path else os.pathsep.join([directory] + self.current_search_path.split(os.pathsep)) ) # Temporary patch the $PATH. PatchedItem.__enter__(self) # Pass the pathname of the temporary directory to the caller # because they may want to `install' custom executables. return directory def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Deactivate the custom ``$PATH``.""" super(CustomSearchPath, self).__exit__(exc_type, exc_value, traceback) @property def current_search_path(self): """The value of ``$PATH`` or :data:`os.defpath` (a string).""" return os.environ.get('PATH', os.defpath) class MockedProgram(CustomSearchPath): """ Context manager to mock the existence of a program (executable). This class extends the functionality of :class:`CustomSearchPath`. """ def __init__(self, name, returncode=0): """ Initialize a :class:`MockedProgram` object. :param name: The name of the program (a string). :param returncode: The return code that the program should emit (a number, defaults to zero). """ # Initialize our own instance variables. self.program_name = name self.program_returncode = returncode self.program_signal_file = None # Initialize our superclasses. super(MockedProgram, self).__init__() def __enter__(self): """ Create the mock program. :returns: The pathname of the directory that has been added to ``$PATH`` (a string). """ directory = super(MockedProgram, self).__enter__() self.program_signal_file = os.path.join(directory, 'program-was-run-%s' % random_string(10)) pathname = os.path.join(directory, self.program_name) with open(pathname, 'w') as handle: handle.write('#!/bin/sh\n') handle.write('echo > %s\n' % pipes.quote(self.program_signal_file)) handle.write('exit %i\n' % self.program_returncode) os.chmod(pathname, 0o755) return directory def __exit__(self, *args, **kw): """ Ensure that the mock program was run. :raises: :exc:`~exceptions.AssertionError` when the mock program hasn't been run. """ try: assert self.program_signal_file and os.path.isfile(self.program_signal_file), \ ("It looks like %r was never run!" % self.program_name) finally: return super(MockedProgram, self).__exit__(*args, **kw) class CaptureOutput(ContextManager): """Context manager that captures what's written to :data:`sys.stdout` and :data:`sys.stderr`.""" def __init__(self, merged=False, input=''): """ Initialize a :class:`CaptureOutput` object. :param merged: :data:`True` to merge the streams, :data:`False` to capture them separately. :param input: The data that reads from :data:`sys.stdin` should return (a string). """ self.stdin = StringIO(input) self.stdout = StringIO() self.stderr = self.stdout if merged else StringIO() self.patched_attributes = [ PatchedAttribute(sys, name, getattr(self, name)) for name in ('stdin', 'stdout', 'stderr') ] stdin = None """The :class:`~humanfriendly.compat.StringIO` object used to feed the standard input stream.""" stdout = None """The :class:`~humanfriendly.compat.StringIO` object used to capture the standard output stream.""" stderr = None """The :class:`~humanfriendly.compat.StringIO` object used to capture the standard error stream.""" def __enter__(self): """Start capturing what's written to :data:`sys.stdout` and :data:`sys.stderr`.""" super(CaptureOutput, self).__enter__() for context in self.patched_attributes: context.__enter__() return self def __exit__(self, exc_type=None, exc_value=None, traceback=None): """Stop capturing what's written to :data:`sys.stdout` and :data:`sys.stderr`.""" super(CaptureOutput, self).__exit__(exc_type, exc_value, traceback) for context in self.patched_attributes: context.__exit__(exc_type, exc_value, traceback) def getvalue(self): """Get the text written to :data:`sys.stdout`.""" return self.stdout.getvalue() class TestCase(unittest.TestCase): """Subclass of :class:`unittest.TestCase` with automatic logging and other miscellaneous features.""" exceptionsToSkip = [] """A list of exception types that are translated into skipped tests.""" def __init__(self, *args, **kw): """Wrap test methods using :func:`skipTestWrapper()`.""" # Initialize our superclass. super(TestCase, self).__init__(*args, **kw) # Wrap all of the test methods so that we can customize the # skipping of tests based on the exceptions they raise. for name in dir(self.__class__): if name.startswith('test_'): setattr(self, name, functools.partial( self.skipTestWrapper, getattr(self, name), )) def assertRaises(self, exception, callable, *args, **kwds): """ Replacement for :func:`unittest.TestCase.assertRaises()` that returns the exception. Refer to the :func:`unittest.TestCase.assertRaises()` documentation for details on argument handling. The return value is the caught exception. .. warning:: This method does not support use as a context manager. """ try: callable(*args, **kwds) except exception as e: # Return the expected exception as a regular return value. return e else: # Raise an exception when no exception was raised :-). assert False, "Expected an exception to be raised!" def setUp(self, log_level=logging.DEBUG): """setUp(log_level=logging.DEBUG) Automatically configure logging to the terminal. :param log_level: Refer to :func:`configure_logging()`. The :func:`setUp()` method is automatically called by :class:`unittest.TestCase` before each test method starts. It does two things: - Logging to the terminal is configured using :func:`configure_logging()`. - Before the test method starts a newline is emitted, to separate the name of the test method (which will be printed to the terminal by :mod:`unittest` and/or pytest_) from the first line of logging output that the test method is likely going to generate. """ # Configure logging to the terminal. configure_logging(log_level) # Separate the name of the test method (printed by the superclass # and/or py.test without a newline at the end) from the first line of # logging output that the test method is likely going to generate. sys.stderr.write("\n") def shouldSkipTest(self, exception): """ Decide whether a test that raised an exception should be skipped. :param exception: The exception that was raised by the test. :returns: :data:`True` to translate the exception into a skipped test, :data:`False` to propagate the exception as usual. The :func:`shouldSkipTest()` method skips exceptions listed in the :attr:`exceptionsToSkip` attribute. This enables subclasses of :class:`TestCase` to customize the default behavior with a one liner. """ return isinstance(exception, tuple(self.exceptionsToSkip)) def skipTest(self, text, *args, **kw): """ Enable skipping of tests. This method was added in humanfriendly 3.3 as a fall back for the :func:`unittest.TestCase.skipTest()` method that was added in Python 2.7 and 3.1 (because humanfriendly also supports Python 2.6). Since then `humanfriendly` has gained a conditional dependency on unittest2_ which enables actual skipping of tests (instead of just mocking it) on Python 2.6. This method now remains for backwards compatibility (and just because it's a nice shortcut). .. _unittest2: https://pypi.python.org/pypi/unittest2 """ raise unittest.SkipTest(compact(text, *args, **kw)) def skipTestWrapper(self, test_method, *args, **kw): """ Wrap test methods to translate exceptions into skipped tests. :param test_method: The test method to wrap. :param args: The positional arguments to the test method. :param kw: The keyword arguments to the test method. :returns: The return value of the test method. When a :class:`TestCase` object is initialized, :func:`__init__()` wraps all of the ``test_*`` methods with :func:`skipTestWrapper()`. When a test method raises an exception, :func:`skipTestWrapper()` will catch the exception and call :func:`shouldSkipTest()` to decide whether to translate the exception into a skipped test. When :func:`shouldSkipTest()` returns :data:`True` the exception is swallowed and :exc:`unittest.SkipTest` is raised instead of the original exception. """ try: return test_method(*args, **kw) except BaseException as e: if self.shouldSkipTest(e): if isinstance(e, unittest.SkipTest): # We prefer to preserve the original # exception and stack trace. raise else: # If the original exception wasn't a unittest.SkipTest # exception then we will translate it into one. raise unittest.SkipTest(unicode(e)) else: raise humanfriendly-4.18/humanfriendly/cli.py0000664000175000017500000002222413275013460020566 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: January 14, 2018 # URL: https://humanfriendly.readthedocs.io """ Usage: humanfriendly [OPTIONS] Human friendly input/output (text formatting) on the command line based on the Python package with the same name. Supported options: -c, --run-command Execute an external command (given as the positional arguments) and render a spinner and timer while the command is running. The exit status of the command is propagated. --format-table Read tabular data from standard input (each line is a row and each whitespace separated field is a column), format the data as a table and print the resulting table to standard output. See also the --delimiter option. -d, --delimiter=VALUE Change the delimiter used by --format-table to VALUE (a string). By default all whitespace is treated as a delimiter. -l, --format-length=LENGTH Convert a length count (given as the integer or float LENGTH) into a human readable string and print that string to standard output. -n, --format-number=VALUE Format a number (given as the integer or floating point number VALUE) with thousands separators and two decimal places (if needed) and print the formatted number to standard output. -s, --format-size=BYTES Convert a byte count (given as the integer BYTES) into a human readable string and print that string to standard output. -b, --binary Change the output of -s, --format-size to use binary multiples of bytes (base-2) instead of the default decimal multiples of bytes (base-10). -t, --format-timespan=SECONDS Convert a number of seconds (given as the floating point number SECONDS) into a human readable timespan and print that string to standard output. --parse-length=VALUE Parse a human readable length (given as the string VALUE) and print the number of metres to standard output. --parse-size=VALUE Parse a human readable data size (given as the string VALUE) and print the number of bytes to standard output. --demo Demonstrate changing the style and color of the terminal font using ANSI escape sequences. -h, --help Show this message and exit. """ # Standard library modules. import functools import getopt import pipes import subprocess import sys # Modules included in our package. from humanfriendly import ( Spinner, Timer, format_length, format_number, format_size, format_table, format_timespan, parse_length, parse_size, ) from humanfriendly.tables import format_smart_table from humanfriendly.terminal import ( ANSI_COLOR_CODES, ANSI_TEXT_STYLES, HIGHLIGHT_COLOR, ansi_strip, ansi_wrap, find_terminal_size, output, usage, warning, ) def main(): """Command line interface for the ``humanfriendly`` program.""" try: options, arguments = getopt.getopt(sys.argv[1:], 'cd:l:n:s:bt:h', [ 'run-command', 'format-table', 'delimiter=', 'format-length=', 'format-number=', 'format-size=', 'binary', 'format-timespan=', 'parse-length=', 'parse-size=', 'demo', 'help', ]) except Exception as e: warning("Error: %s", e) sys.exit(1) actions = [] delimiter = None should_format_table = False binary = any(o in ('-b', '--binary') for o, v in options) for option, value in options: if option in ('-d', '--delimiter'): delimiter = value elif option == '--parse-size': actions.append(functools.partial(print_parsed_size, value)) elif option == '--parse-length': actions.append(functools.partial(print_parsed_length, value)) elif option in ('-c', '--run-command'): actions.append(functools.partial(run_command, arguments)) elif option in ('-l', '--format-length'): actions.append(functools.partial(print_formatted_length, value)) elif option in ('-n', '--format-number'): actions.append(functools.partial(print_formatted_number, value)) elif option in ('-s', '--format-size'): actions.append(functools.partial(print_formatted_size, value, binary)) elif option == '--format-table': should_format_table = True elif option in ('-t', '--format-timespan'): actions.append(functools.partial(print_formatted_timespan, value)) elif option == '--demo': actions.append(demonstrate_ansi_formatting) elif option in ('-h', '--help'): usage(__doc__) return if should_format_table: actions.append(functools.partial(print_formatted_table, delimiter)) if not actions: usage(__doc__) return for partial in actions: partial() def run_command(command_line): """Run an external command and show a spinner while the command is running.""" timer = Timer() spinner_label = "Waiting for command: %s" % " ".join(map(pipes.quote, command_line)) with Spinner(label=spinner_label, timer=timer) as spinner: process = subprocess.Popen(command_line) while True: spinner.step() spinner.sleep() if process.poll() is not None: break sys.exit(process.returncode) def print_formatted_length(value): """Print a human readable length.""" if '.' in value: output(format_length(float(value))) else: output(format_length(int(value))) def print_formatted_number(value): """Print large numbers in a human readable format.""" output(format_number(float(value))) def print_formatted_size(value, binary): """Print a human readable size.""" output(format_size(int(value), binary=binary)) def print_formatted_table(delimiter): """Read tabular data from standard input and print a table.""" data = [] for line in sys.stdin: line = line.rstrip() data.append(line.split(delimiter)) output(format_table(data)) def print_formatted_timespan(value): """Print a human readable timespan.""" output(format_timespan(float(value))) def print_parsed_length(value): """Parse a human readable length and print the number of metres.""" output(parse_length(value)) def print_parsed_size(value): """Parse a human readable data size and print the number of bytes.""" output(parse_size(value)) def demonstrate_ansi_formatting(): """Demonstrate the use of ANSI escape sequences.""" # First we demonstrate the supported text styles. output('%s', ansi_wrap('Text styles:', bold=True)) styles = ['normal', 'bright'] styles.extend(ANSI_TEXT_STYLES.keys()) for style_name in sorted(styles): options = dict(color=HIGHLIGHT_COLOR) if style_name != 'normal': options[style_name] = True style_label = style_name.replace('_', ' ').capitalize() output(' - %s', ansi_wrap(style_label, **options)) # Now we demonstrate named foreground and background colors. for color_type, color_label in (('color', 'Foreground colors'), ('background', 'Background colors')): intensities = [ ('normal', dict()), ('bright', dict(bright=True)), ] if color_type != 'background': intensities.insert(0, ('faint', dict(faint=True))) output('\n%s' % ansi_wrap('%s:' % color_label, bold=True)) output(format_smart_table([ [color_name] + [ ansi_wrap( 'XXXXXX' if color_type != 'background' else (' ' * 6), **dict(list(kw.items()) + [(color_type, color_name)]) ) for label, kw in intensities ] for color_name in sorted(ANSI_COLOR_CODES.keys()) ], column_names=['Color'] + [ label.capitalize() for label, kw in intensities ])) # Demonstrate support for 256 colors as well. demonstrate_256_colors(0, 7, 'standard colors') demonstrate_256_colors(8, 15, 'high-intensity colors') demonstrate_256_colors(16, 231, '216 colors') demonstrate_256_colors(232, 255, 'gray scale colors') def demonstrate_256_colors(i, j, group=None): """Demonstrate 256 color mode support.""" # Generate the label. label = '256 color mode' if group: label += ' (%s)' % group output('\n' + ansi_wrap('%s:' % label, bold=True)) # Generate a simple rendering of the colors in the requested range and # check if it will fit on a single line (given the terminal's width). single_line = ''.join(' ' + ansi_wrap(str(n), color=n) for n in range(i, j + 1)) lines, columns = find_terminal_size() if columns >= len(ansi_strip(single_line)): output(single_line) else: # Generate a more complex rendering of the colors that will nicely wrap # over multiple lines without using too many lines. width = len(str(j)) + 1 colors_per_line = int(columns / width) colors = [ansi_wrap(str(n).rjust(width), color=n) for n in range(i, j + 1)] blocks = [colors[n:n + colors_per_line] for n in range(0, len(colors), colors_per_line)] output('\n'.join(''.join(b) for b in blocks)) humanfriendly-4.18/humanfriendly/compat.py0000644000175000017500000000703513322466321021304 0ustar peterpeter00000000000000# Human friendly input/output in Python. # # Author: Peter Odding # Last Change: July 14, 2018 # URL: https://humanfriendly.readthedocs.io """ Compatibility with Python 2 and 3. This module exposes aliases and functions that make it easier to write Python code that is compatible with Python 2 and Python 3. .. data:: basestring Alias for :func:`python2:basestring` (in Python 2) or :class:`python3:str` (in Python 3). See also :func:`is_string()`. .. data:: HTMLParser Alias for :func:`python2:HTMLParser.HTMLParser` (in Python 2) or :func:`python3:html.parser.HTMLParser` (in Python 3). .. data:: interactive_prompt Alias for :func:`python2:raw_input()` (in Python 2) or :func:`python3:input()` (in Python 3). .. data:: StringIO Alias for :class:`python2:StringIO.StringIO` (in Python 2) or :class:`python3:io.StringIO` (in Python 3). .. data:: unicode Alias for :func:`python2:unicode` (in Python 2) or :class:`python3:str` (in Python 3). See also :func:`coerce_string()`. .. data:: monotonic Alias for :func:`python3:time.monotonic()` (in Python 3.3 and higher) or `monotonic.monotonic()` (a `conditional dependency `_ on older Python versions). """ __all__ = ( 'HTMLParser', 'StringIO', 'basestring', 'coerce_string', 'interactive_prompt', 'is_string', 'is_unicode', 'monotonic', 'name2codepoint', 'unichr', 'unicode', 'unittest', ) try: # Python 2. unicode = unicode unichr = unichr basestring = basestring interactive_prompt = raw_input from HTMLParser import HTMLParser from StringIO import StringIO from htmlentitydefs import name2codepoint except (ImportError, NameError): # Python 3. unicode = str unichr = chr basestring = str interactive_prompt = input from html.parser import HTMLParser from io import StringIO from html.entities import name2codepoint try: # Python 3.3 and higher. from time import monotonic except ImportError: # A replacement for older Python versions: # https://pypi.python.org/pypi/monotonic/ try: from monotonic import monotonic except (ImportError, RuntimeError): # We fall back to the old behavior of using time.time() instead of # failing when {time,monotonic}.monotonic() are both missing. from time import time as monotonic try: # A replacement for Python 2.6: # https://pypi.python.org/pypi/unittest2/ import unittest2 as unittest except ImportError: # The standard library module (on other Python versions). import unittest def coerce_string(value): """ Coerce any value to a Unicode string (:func:`python2:unicode` in Python 2 and :class:`python3:str` in Python 3). :param value: The value to coerce. :returns: The value coerced to a Unicode string. """ return value if is_string(value) else unicode(value) def is_string(value): """ Check if a value is a :func:`python2:basestring` (in Python 2) or :class:`python2:str` (in Python 3) object. :param value: The value to check. :returns: :data:`True` if the value is a string, :data:`False` otherwise. """ return isinstance(value, basestring) def is_unicode(value): """ Check if a value is a :func:`python2:unicode` (in Python 2) or :class:`python2:str` (in Python 3) object. :param value: The value to check. :returns: :data:`True` if the value is a Unicode string, :data:`False` otherwise. """ return isinstance(value, unicode) humanfriendly-4.18/requirements-checks.txt0000664000175000017500000000015513055423433021322 0ustar peterpeter00000000000000# Python packages required to run `make check'. flake8 >= 2.6.0 flake8-docstrings >= 0.2.8 pyflakes >= 1.2.3 humanfriendly-4.18/docs/0000755000175000017500000000000013433604174015530 5ustar peterpeter00000000000000humanfriendly-4.18/docs/images/0000755000175000017500000000000013433604174016775 5ustar peterpeter00000000000000humanfriendly-4.18/docs/images/html-to-ansi.png0000644000175000017500000001435513324610233022017 0ustar peterpeter00000000000000PNG  IHDRR[ϚgAMA a cHRMz&u0`:pQ<bKGD oFFstIME .C2 vpAgǤIDATxy\SgALXEeĭ"wZd7Qg:tXq8Seh40EmWj-5la$ZQj5O'Mvܳ<}rs@AAAAoN&߇pbN7pŋWTTܽ{=,,lF0`ŋ 3&44ʕ+7olįszwww~~~zۛW3 㸺qalSA^3^^^Ɋ+vuҥ~޽{ժU- 2w{֭U wuu޾}{eeĉg񚇇O0gϞ ֭[nϟp֭قsNGAt($իZ${:.55uʔ);vxA׮]~BB_ׇ|葇o{&%%}z~Ϟ=B BGEEDѣǝ;weA3$$DQ'v$_.,\}d&Mڻwo@@@eee^,1v( H$B 4MryvvF1?w\LL q/իWboo#Mt'7ھ믿rcܼyں[nz$EEEcǎ֭ڵk]\\ZTT$bV___t:^]tN7]v-775,Z999.\زe-[c|b ^aZZNضm[MM͉'xi4;;;Ryk׮=|rs蹹wڵrJc;;;5g =...㏣ϟ?C}ܹSO=eX[[kkkaæٳg s>}k?P(R޽;O>d8qbر>a7{TXXP(.\hee$g5=ztzzkv=GGGVkXy{{kVUUNNN>~876lذvں:J%)vܹvVh.\8q*888++̙3?cIzz6[nn!ѱļ}) sK.3闕dܿ,[L'~zΜ9Ort:f#G,++7ٙIZ-˙J7o4G۶[js3f 0 --޽{"ӧO0uԂ!f̘!fx:w[XXq\P;wǏw2mڴsαkcvm=z}ﵵ2y'[oUWWwȑٳgrJC/=77744 A f4 ~gXI&>|?쳎nAAAAA4K=󫪪ܹzno[_XXɦ&jlll,T6Qmm-{/o(PFBG"G_u;NъeHHH0_4 5iڵ;wd rUD2eʔ/ &&&J$%Kdff2+|ٝMgggk ''/e_RR2f̘]n <:00pŊLxY[[[VvƆ:::zzz;w6m֞>}z_~'N󋌌YvQ'233K~- $4qMMMw1b$[ġCG-DyaNn}f裏 څYSSseߣGo>n8!&9s]EFFnܸ.,,ɹ|ŋ[ܿ?88V}tiӦMyyy9993fصk `޼y...?Cuu5;'> @*px ̫'ɯ"V\3bĈb&<{.>>wիoob,>5>}zMM͞={lllbccSSSђ*ˣECn@&4R|!ͭ@0 m!_E탃8#Ï,,))aٿgcÇYOv*g!B7nxWt:YI٬Y>vDuÀ\\D3PqH~Ac읜ƍq\CBB2sNz#G׌}ۇB>rL2t+ٻwoV 7L(R@<䦨}"?ny"U!鲟9s}BCC XxTTTtӧO\tI'obTyu={|7>c$ y` S'@0%}2㗷̛7ORT)H_SSsС/w-; | )l~'';{6@*0PTs+`\**++'OAAAAAAAAAAAAAAAAAAAAį2oooK4F޽Ç{rŭZJ\牼q9??ŋw^ٶmۏ?ѭ cegVVJ:r#Ri3h"FV ܄T(aaa֭۹s'*J+++{)V09x]]]9S*CR ,'R)022RXY53YsPIc |?bdw~9\ԫ cpj$%AɃ"z&l; D~4s":IǚB+'Rdذa6oB'666VںsקBl5:88 w^!Bux $deV o@Y._@@<`N =l5ol# #PU06N tzcϟtw_&:ϟ Fo z@lxrXt0as\\ܱc^NW^^n,ٷo_ZZںuRM6eee]pan߾suXr; [ZW$P+ |B:uj͚5 s.8Q:8X@;2pgc%ddO5v)RV߹}cl!UWH;X; 'Skׯ-, :::!!aɒ%JP|!UUUdǎ'Nt 9VoW fϞ*.[̸ %гH'%kYR^ЀOgϞ `cc3qOՙ,,Y^^oܹsڴi", |ARRR\\xafif+T 1113յg6P@AlK df"(l.O?|N5c\m>T jqp1Zhh9Ap0K5Bh@vFjڨ(&=ztnnZ>|xxAْpNNNǎ27ܧB0?0|2(RW@"[ljDZ*yY^R #"""//[zRy>f8HkzBUR栫IPR!%]9-(b[МH;`x!">#O<1/ 汦;wX`AG7 vAP     x?瀓)D%tEXtdate:create2018-07-21T12:30:46+02:00r2%tEXtdate:modify2018-07-21T12:30:46+02:00/IENDB`humanfriendly-4.18/docs/images/ansi-demo.png0000664000175000017500000027703613226540046021375 0ustar peterpeter00000000000000PNG  IHDR i=AgAMA a cHRMz&u0`:pQ<bKGD oFFstIME 9 vpAg& 1DIDATxwxTߙI!H zQ{ ^T@EE7*Ez/JHB(I $!!=?I p}#kά٧!B!B!B!²n4|bJv&u/߬ K-FnǠAOw6}˙(yk zou{Hu1RA!r-111)C)TjRbbB\J[#J֤<,8ePvA!Dn69=1}ug7dӅ5ԫu>|ת GݟT;eߢ N{ׅy4޽U늠k5h?W[uѪ{Ftѝzˀ.<&CǶٽ`\y1æo=FnWGn conW|o&(kRBam7ЙLb'mW*>2:M%}k+E%m{.|/BUھW:&I6(JNLLuqq|W:V)sRt x~_:?#4vgZ':Ϋr7~3zAR@[k"Ba}}'wg޾O:t̍1J\NV V-9%6bT Mh>u|VQoW*aM_ۯ17m6wTM{k BQ\;LT*#6*2S7:n)RthsMQJ)s|u]TfO վ7W;y)lͯW!D6TIy7_u&IUJ)u}逊;YnP JMƒߚ/F ںǵռJ)ul̶?}9_d~W{|?{XO}tmw~2-^:xj"'L}K 綠]͉2u`C~[EO[<{?W[olj>ܾC?cёkjzB[^עq1=Wӕʈ 9:t Sl6U61vjRS1 λԕ JC q\h0K!_K+ZBCh-!bpV@$Tc$^p[n A|X;@c ; )B7_zW4`RoQKsJ5+Ja%@$Ա !@a5| DIU%T~8B=h[sZZBaIwZp%X  & t[UPT8 QU!B!B!n9dRSk7@!ʞ~B2O!9=,܀PB) c>> B]fapo2/Det1pTc8~W9_Y:I-=4v7&ac^ @ 37P$ V_*w& yW!R h` &ǐ ʸA/WKK_QY!X=&B9pv0dp5߄WYX?ABd.^{r0W_6Omr /B4_%z=LC!D"YY7Π!B!B!B!B{AXhE,}"W͌{A׫ !徻Oi^ΔsBB(gɬ PuId?ȳz昫O{TU!#w}n)&5]sXaIMoWއ 5Yޜ0ksB])i{02B#pnsuIQW"^!(Jmn8RX `~A˛%d")F]Lorϳkn^B!RQ7A9B!B!B!B!B!B!B!B!B!B!B!B!B!ozF>ަM?S/m7iIU?kb %ϖɱ֠V[[2BvfJzk\.Ug \Jͳ˂c!QNmBa 6yzQ_s-=uݶcэkv&fPkNWкݵmwvm5ԫu>|ת Go'ҜԤ.n87 t:rcP,E7F{rt޺9ۺ Ց;7۽/c3f:BQt[,5xŏ'h1c~GL|[+&롉Gn=W>Xgs`=51vl~ MMϽ4V.y7?W^ukϜ0vK|1kd];_N_q9q_th/ۣqi{Ǣ##nBuyS=XDX{532@'pCtXV6>G!JI_sa=(󲟾Zk? PF a(  (  8 p Lk45|dv(0Dw_+Z7sYmI2nsK+xgX!P+;;Dfnϫ | zUoCEڟ%V!)WV$T)~Nǂ`x[Oflz<7+~BX 42`.tp ve|w'(8C]s2<aݎ̇Zs)p|r|,> b(y`.u?G!Js,3NCh dw._`^aAsm1_m\ν _Tη]{*<Ȫ]7+gYͻÄ}1"= z.c+P)/H߀;fMT(_EGD\{k\M4 ~׃Gu%U3h:iys_߿~_ƃ=hkͥNu{Rz`kzoN{ά_;j۬o3OǪ}>ζY4~_xowdH [2\6]7,}ն|Nߏ9=ڥfݸ lj][p#s8e٧V_<f+u3 _r߲٥^U<2g'o܌qq6pGq7f@!mѥͻ.C;jX).B!B!B!B!B|:e8<{`f`>Azke[~UvЁG# tJq ~^B.ZT# 3k0`qV[LZ#75eTZJRR\{rhM]!uS|KO͢S3>jU=dɡkt@|5W ۀ*~' @s+pf+}\˂G=.t;f]ͣÜN<(j}~ gky[  y:|pz qOuXrn2wU_nڹkNiޕ]n$wp`_6R< 8*z+\D\{]Ê2,j[۴jZȂ;jժ#Mk4He n)zf [cO0mqY=.Ľ.a%#>2ŷQUg}?Y\c[GlrZw?МZZVkׂ @7vRZB-ހb۪a:afpfU4ƅG:$٩>[E96b{oS1[^CC/i|5 ~d8h`k̀w!nx4RbZuWoZy+4+-/!JYY} KLc) Rr͙[ JfZ'n9;ro(KUdJq!qEvxw !~K84T<4ͣӢUqҹ7{a+WJ\9s-Mg=2S?JJsr3׻P|ޫ<]DR8;W^S&W7;H>lZ& cߨU )G;8-+zW\t}_b;V<`{I&/͞с ?)kn}k8o;a2_uw2^PҹAelx*ֽ)1J?vCw /L`cD˟yWO]xX%Ji\w¡2>ve"n)6\je"n)iR r)q!%4NӴ& ;T)H+{]lt7Unw7~-[;yŹuרj}5ޘJ7+-/]M#'n]}Tr]q} rRzCڿs [Q7n޸L>3}{ucSr#v[6>4ҫwK#_RXqJq p"|z O㢁3S¾xfN]+Ņ}֏Rv!{jO߰6OT:,B!B!B!BQZkY+ vQۀT(8eoa%Y Z+n~ Qʤk%h֓U7ZqK9 g`, coCTZ+.=s\]oP]5AۯMoqtM't a|֫[>k 08֎[_̀pqKɀI0~lShنsZW+yeΑΫu<iqx;Y:챡p7Nz /(qK ? _.|R6B J:7|wgSVbґ飧qpn2wU_nڹkNiޕ]u)YuS6bA FRl APو q/(ܠs ؔdϐxsj4c8-dZjꑦ5j]a((^644sVZq!% qg l]ޱbǩ '?P`[GlrZw?МZjoq.v4vR\9;~JTR;Z:vA6V*=2$88ٓz-_ھȫcxm@RX9,C[_qsF]gkfJSAUu6b4u8N0z⦳jrj4շx;Y5n~Jߘ yc~zƅZkvv=qzz/R"_%.`/!*>xÔGV*C8}J 4^eIǦ~-[;n_k]|Ϝ5{fz-_*[s\5YZ%n~ږЕcVnvV[_9N_]yV^vh`%F !I !B!B!B!ݹδfƽ_x P&U eո/2TjRҳod#oո$0\UXV[J>aո נW֌7RUFAuUM^ Yݷ= H!@R> -e@\{Xm5A;SҮޱgE j=_`&)3qK)e&n~=LRXRґ飧quUaT&9 X+n)uh+ [+.Dgxsj4c8aWBkXM -FTIFC2PZf3uUaWh)8 =#h4l^6YUT VBzƅWܠb3i.^>.kJ3mkbڜ6p=w[y [AP[5n)|V[J=Ӡ񪆵B!B!B!B!B!Oɟwl:j{Oi`= y/j:M\5gJqj=|fKQG.XAhV[_BXG) s |rFG  pj5T->nzdUmX}Ӗ՞ V[_BXEavbu;[o\ξ۾mLpdj~8q[f zq WӏPd3`4ah#Cl~\=Wk]^佾U@R泻=͆e n~yHܹ{#|ԩ -/!Q^Yضb }z/|uДKwuzhN&N0s`&Z#?:ke"n~tDbrkm[3G_k-/!JS kf$K hPJ>ݽoƍ<.ZFgd|SxrW7s\I^3VBRl|q쓏b[Yr=1E+ q!%Ц\:tb-#W`Zq⢱vZ+I @Zu MAys戹7~[-zR}ZObN`kU'iDL-;Dž|ⷍ{XzLEuY7|>s/|ظw|UKjjF̘68As1dƜZxX\[ܷf.:>_/Y+n~:g\zcp?UQ=NU?NWp%#bE=M&k]|Ϝ5E=zuWB/\8 _Ʈ=#5Rbt>ݿ?y裇]5Rb_]+_V[_B!B!B!B!B!<,8eH5Us2_@z.:0yi[+n~4-+8Y3n~R}}dCVR:J;yu<s_MzO=ymvr:5Ǽ dg\jy ֎[_Bph:xhGE緍dK xj֙{뿖:'֑܌!p;hk-ׯ?gЋ3Y˪K֎[J&7%?㷀VUOi zQ/V.kȧ~j[|z>|q z4g[/o-hg{q*q;Ik\~Wو[_B55D{{S+g_cԋVOѻyDd_tհN_.Y%pO֊[ uղVRli=B J^:l?cG~ձ/NTM뿖6Eд7z-Ż#4^GרbkT$S=z֊ qO!Oq)$-+xM.c^^)lJd{_=kQý|< 1䄮m״M/Zz&7NFgE9б Wsj0kX\x9|%snjRbrg ~{UsXŜV ?.S?խ|f+u?6~n ^n; xk8Ҁ)o`͸8 3)([5Ww_@\D;wk-/!cV[?JB{]Iv ,B!B!B!B!B!B!B!D1_s*8ӹ]ӛ9J\Z\!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!%}kn?;kzQ ċגUV{RR*ݟےs'oyp5q=`a+RЦg&tW_| ߺtk]ۖGr>qͮD`[~G-{=ñ֠|d~|x_]iЫ]739ٱQz5 7:y+7 t:rcP,y?x7ܣU-萣;;<4֝M!]yKϾ2\%nm sWTJ݌JTJy>JWҕJvZR3;Ն-ԥCfNzz5ԭnk^v)25ǯQ*=:Rnz6bTyv{W*",!+u\̬,32:Ny`nys[|):UZN. R'Z{"o: J)P)s3dqB>dz;sS/+u\6ᮃ!|G+*ppM'wȅ}lqVAҪjE1YqͻDu㗾מw8 *|.QSלe=@^soVǒOhGs଻&#-meD⾖97o{T̒S:j:7x )Kbv_ԼoP!6ro٢汖՜M}=lk|Bu qvܺMV_ez0//wAsi(9r.B ʈ;lYf1vl #F">:@Zޕ*#5Mw9'ĥ[iZL̶nj_я;W|Um\6&4rn6!ݕyKRa9[xC%SA)`8xeRϜ0vK|1kd@湑 5¾Y^lNRXjyv[0G=bʮ[Ǖ~G|WJX;3Rgy}s7D~҉ㆬー֛9%32 ʂ]:_No֤?2:JԷ#F*p?=Bq7(ͳ'6u2TD.5ͱ*zAkWtnaTTJE#ڂ=SJVG=쮁޷qJ%]9JzJ~JoT3+^\8|S)CԞ87wG{R qk2ȚRJ%ݹlfy3yPxA)uh:&8v5_CW/Bq|-c{ ѥߋ)uafBO*~wf(⃷~1\kB!B!B!B!B!B!BQkNg:kzf5%KDžB!B!B!B!B!B! (v` A_G!NdTF)H $BB鯷 ChVQZX8^(( [ͭK8P ] ΂Yh ߕ&k܊Яꠌhs MYASPP^/E}?EgSN܄ &{z(2n@X@ih v0b=lo%kOp=\(h@-=@| MZ ops N?/tXq4VaqBeѹDyO_εkny vA(JaXC ㆢI$d 5x܄DPY_ ?9c\4$%QiPp L6{A,H'<\ B2si[p[||~JTR;vxw !~+G쫏B/=fBq7 |'A/ƿ`?~6`zUJ:]>;p]CE[wۏ\x!c&;H?%璸jͮ|2ּ;LhaZ_u[>rI  v4}CS=`B`k\M4 ~׃YƳwFԕx`W!i1Ge<ъ.!Oصhw7=ws!ү mַc>F]g[} ~_xowdH [2\6-7 !Dֵ.] =2X}jpig6R7 ){o-O]U#svHwna+j}~i ~{}<-Kf";9E#"'Bd{XcnX9HHrNIk!9 "2__zJ#uXBt밺Y;8C3NV{ ?Bإ=(ɅjvVs4s;D5{lh-=4Bu밚dL_n99]㖡yBuX̷+ʐn ܪo oªi!  y!\FUdsAkt:`˘׺ـк]U)T:$٩>[E96b{oS1[^CC/i|yB!?{Pܟ53F([7/^,L!I΅B$sBȽv k2yT:&L!IB$sBJZpW%(9nBowמ r]tCNC<:-:m\e}ݛP+Q J%:6}^Op(*)%\ZBXKۀ~oT*tx|r?[{+.e:>߯}1V=$Cf@φ{.!*ܠ2prҍT Qyb՟lͻÄ}1"=W<䫧.|<ZJt6:Mf_{IODZyVΈo*K>%I(u{? ~_xowvmC):N~Y?dNv&ԶYfU|um!9zCڿs [Q7n޸L>3}{8){o-O]UCf!(cJn 1K7 Xӱ%VFH͌CjfaU|]I_>AΊH9nªNѢж̑m!"'B$uXJ# B!r*ܠ9{}aWDa ̋נW֌7RU8:~:Ka{>URU! G6LuӺ_y+#2Ls{uG;-O|9KxQ`՚=6ƑBqוV/劰bґ飧<77s1~Bv[qgUn.K}V!(u%\cCo6j9/s=9;N8-d93ٸgB qg l]ޱbǩ '?Xn]ekl@s hݮ]q!w_I7$ƶFD51F'dSb3i.^>.|B!cߨzҩ7ұw)g6~&ڽ*EeJxtr;VU+ΘK724}B7Yq!_L!JPZhrPelr)U_!9 "{3l_;Z =_}/!9nBS ˎ/zn7M҆[o䐕ٽ _Tƽ<3u[V.h†S3_gOBQX%>nW40k=۴it}_b;V<`{qqsu[X5=®+5 SṷW!Dᕼi94]_l ^ͻÄ}isu[-TzB^熌QI C 1w6U3 Ai EjH7h>+JZt36t瓟ߨlZ:*9"L=Tl˻^!EPJs){o-O]UC뭿{tЦ=st+ve{n̸;fuaS !D'{gE# w7a]wgelwH !9 "'kau~)B7!ȩs5h5#TQP]U:WBV-/ !DcWי/>y4pcI%suUMU22BQJ,rkґ飧<77jBs!?ӁSc󪫚_V!eDimU5[U!DQZsuX{ "2p=lUS\UMIBXB B!r:ESek7!;Qe쬒&g&9nB B!r*:Mg=ZM<_ts) G5a;/ p~ !*n~s)%$윒]GXw֛g&ױoo8g9áȄz5ڢ]ЋN?~&]Є mg6VxTaBR!VjЭRwݪ8v1x kgt fFKw[}燦eOtn|aѩ, !(=% O =.v}#};6qkYu3"?'۩pzꪹzB!JO 熔KT|ah˫ -zʠV'~2^FH>%IAfA t} W=W!&е횶CKOȴbRa*衪`^=s2SU!D)*ܐ~(V_ψۿhR䡋ɅzSv;㯷Auo4v|=W!G*2V3B53TI !J…B$sBQP2G!!9 "Ҫje(C!%k]OxqxF#^W !  ?8C @q-S?UsiCa!aOKBQ2%OΝrck؂bWZxť ,W?չok<PZ'όX:B!J Qyb՟lX?uvs~=saчGѝꧺ3^kO[O?潿~k(~W!ijcncSY5mjv+wdy]ie?wW_HRJ]: ?c5N0.x ]Q^i1^F ^j^!ڕo֭G*!Gvm?ױwZG7FnWGn ʕ%Zwo]vrֵ}m@\C|ӡ.Zո_+?BhmzvlT^ YA^Vm8^ h.-߲ʻ6CnnԹGZ!Gww-:ݻj]t ֐ʝWVsX\l]z2A}[?>vȯˮ4N jyOsyxz~^;|͔?%4hə=|lBY:P,Е{0& |u}yڐ.|Pi_˶}|,]k%+:}cJ:?9Ϋr?[^KԬ#g:N2 ]ORJvZO`߱JGF)OvFmkL)௳):SNQ 7X-)k_l5_=Tҏ}3sN?;ksڠ4kՍ_c_{⬦wFso#/P)aQLc/ԥ^{<6Uܕ"ŵ[*OP??۽}+u*NAoQ3UsYOhGs|{4dzs _8K1{:T%}>7xS>wš鬋*m;S6wTM q_ѹ|@ qp՜횭3NQj`1ucj8蜛{Z(x Mg{g. 7F_>sq [<ֲS>:N2Q3o5l4lFȇ׼oP!6rlDN+ʈznYܺmHQ>ms])J)b157+?uP;憸_xn]&ӯײl13fǡ}Ƚ*|NOB\6G,n%}z◰/GV|:(Tm ׎s6_H6 9c>s!9xd?ZHлw%#>.ނAF̩[^(ȋHK6@FBTBzA5;7giIitM{5}=#!*cV(_|+tJ~6z \uΕ*62yUb7톦)lmLn:G2ۣmէnT1q?AMimyrq({5Pk 07}9~СC:S}߾xٱO:wGRWn70—Si[ ;4vV߇ LBdqju2e<\y7{R*MԺ}FsԏGn(.t`þJȣFV܆U7k{xR?Zo;,ol~oJ8rG{RM5}jR;=7x_NOi\λԕ JC 1%vjRSQ`Si'J7Ǫ1?7m_7ҧ*hv8Z͵Ŵ+& !ʌR|g | I\:|:7¬/8ÎRm CJ% F:_}oo, -;ԕPx[ ^ֳ-_ \^IPMSKbLZxP7yVau)Jl*hF=R><y}yOa? sN)E1=N@FʁyƦ%}߿MBan` LKts΅ptaa9:V1˭?g'dqzS0' (sL1 0fCNܨQ>o\?@!wAx6DQ:S?e$סiٓ뻉/$i~sӶ+rAQNh 0 n[p6}l0ox%v9w M!Kh /> ON3qV Gx]CwbSB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!<}!vѭS~ʅ+ 8=</=v~mKqRr8m}E`W̄mόmɹ5KR}M>{zzcv,$!Iku{i]ǜ"p qO]^zk)-mg qz%D* ݒ_? TwdD56VJ%F^"yɂ<,8ePv\.Ug *~l{~8I).KWP{SJZխ1gn,=|F 6o9ӢK7^7Ë"Mt>=\0k CRm+U0$VͺhY66ȮC3:n[15Bb6>(ЭȍAfo廃oa^M=?w]fL=|lBY:pMr}S⏯ءϝ'^Rԅ٢[>y"q+zdBg6v9~}R7RuyuR5sk^v)u~[IcIJ)î~c_5הRL;֟5^4")_Οmm5cJE&d5;^pd.}4O1s\7@LA)Trbb⭋;dG=fXTKѩnzmߗh+ݙ6JC35W"N%ndzTzAR(U:SNQ 7}-)k_l5_=Tҏ}3c}_V[5йTmP]wߵ/}뱯= uqV;&/7RMiUâB Ǧ_|MK߽0!xzC{m<+]E.gkUܝ"~~{v? VUl<ԁgf߀enCUa5\*٣^ȱ6yti4Ce#<կ<y\,Q'4qף9Tps|VQoW*aM_2=̯84uQm{gAk ~;fӻM)*UsnU5[W&f'+&cJp97yRQI LǦU=3yRlrM_>oA=ye5'|s:N2Q3o5l4lFȇ׼oP!6rlbN+ʈz%uې{_xn]&3oY̽4y^{#(burwVnnn0sZYsmn\)J)b1o{0{3{UPWz9/վ7{SU !\V6G,n%}z◰/GV|:(Tm ׎s6_H6 9c>s!9xd?:Hлw%#>.ՂAF̩[^(ȋHK6@FBTBzA5;7giIitM{5}7#!*cV(_|+tJ~6z 醼g:mfc6OOZs2{ dGiqR66wsǝ+Ul>dĶo.EZz!=dET'˟Mba!Z5rn"r vAcϚNm:tNA`_g>gKg7u瓁xfSfM:fu?T˸~^_|愱^Y# 5xŏ'h1c~GL|[+&ߤ[͇wh] |o=51vl~7$͛^`鞍r89ljXw8WkѮ]jw{J6 +)e=^5gdm1{:VҡUlƭm5nO^ᔿ]Ҷ!M]s.ٵphȈ&a5N-TQTްxgӬ!nќ?JK;>Xs:jR*%袑U3=EdM3Jz}sy~GyΫJGOE}>`.λԕ JC qʷ=4ں8f[cT:ޓ]BXoc_~.ŭdSM!M!B!B!Nsj /Nz'|7]78W`7T) 9 a ˠ%Bm}yi9*NsxvCVV4Az}O|C-Dbe't/rGxIXI+[ _<\%64ܾq9; !n+ zfm9_ݶGya ,]5w !3 U&(Ln*\.-;Lw0&G9UÕG]]cՊ{oviTk]3=Gsa{ zez4_watv/$9] Л{^{ :iiCU+WA:;$]r+ 08 VC ?< G޾@ =fՂm~83I\ySЦ8n3e)r0ƖB qE?BB *?Sۏ{bo KKP=<PU乵jQ醢͵F峩<}Z7Ϳ/^-hl0;:">ΏP yM%ZA3|liש=%! =7h΁>tkڡoɰ?pK`,"i^y{*޷D5ŪlC?EKکh64-42Bv[kS.v_m^v={TAA1^Ǡ ^xH(^"Kqg.N$˩4*ƻќ&S83[*Qg{uʑ +ܠmmt`b+}5tX׋փ !x,CsռfCKv^O8jQ7A)D\2.ѱ< dz=EΜN)F&{Fnz)^}`:zW'MzeS) Eo FpBAA'5F%R{X((ETvcqslk=O#2 ̢6>PC<7YM@p0ъ(""ȹ Y\G Fe̎.kZ{XhpphRQ!v9_z2*>1)fȶ/FOeB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!)ֻx[@D*5`J<׫CS8?إs󪎷.ٺioXXk zou{Hu1k *ݟAɚs޲cۺ%+&@S6}˩[&c|޻գxd-0][RRQRGV܀ꎌ_:fƿJ+7U7;Y9gǂC J\ڣK%̛Ao')_zzi3zO|'N)qo]/$)m[o!,BsաUpm]JY Ț,@CwU]9`^ؕo&ٳE5G.0~HjNVyuRۦw5CVn_~mҨ=i]՞Oܲݰ>i lv/r = OjeSviv׵v;wee x7_|fT՜Miiھd:`А1=kqt+]Ǿ SlrpWPF³׆uÇjֿOz^HcJ^|-YЙk_TztMy|Dm25hRYG8׻u^d<2˯=^sM)u+**4cI[㕊NS*rI֘xH=-_9z85a UJ\n얭U Vĩ ʛέD+O ,[B@r)r>tu 5櫧J8BzJ~: j@6Qoy='.jz{>&oJjn86}kJ] ڛoS]*ra_?[\;e̯ ۷iR`st [<2:N2Q3o5l4lFȇ׼oP!6rlskJeFEf F|hn6K6s,\)J)b1nD=~OUs!]kӦ%ޭCOEȊO5m!qpn Ɇ[!GBRptgdH^>d$HY]˻bAF̩[p׎HK6@FBTBzA5;7giIitM{5}7#!*cV(_|+tJ~6z 醜wsя;W|Um\6yt}9_B\B2zSQܗ :tС;j}ݗ~틷sw(u֙.-? S*|C?u/+^ԥ3'_`}RWүJT kWs:I]Y0KeF.$ nW'mYwl6'\U[p,|3)a?=^IrӂcӸc|xFgX6՚_f0WW^W~0eeF*p?I+sju2<䛽aJ)ϦYCj9ݹS?JӁ <7s{`*R".Y5sjW߬JgVh9m˝+IwaYBsz.)pv̏\p1=Wӕʈ 9*uzJlP̍{R;{Cn?uxR*=Ђqujy-:aM?j56=^0>(J G9o`J3ϊ!}Bsn̲0Tr<mkCSR?+"oc_~'lʷ #:ɼ B!B!{/$367U aQi M:(v} =%i07yx<w! {xZP\/X O'p` @2l|E-ƞp!`c-p8Ξs/%6$f 2fxS W+2д!JP xm<$OKBZKΏDlPi{ /W)R<ޡ&gnnB1T z܄EpXoMcgϭp!NΑh`C螸Uކ&,MJ~`8\] ߥ6}^yf:@;˴_ N02{^{p74ou.t j"-n'e2g87?x &yv+fŋ W;͞6hfuvl7VӻYHʙ6igj:J0>^:hnO z ԗ@_43~u轃æqмiACs{Pv{"T A⎏^At3IGyq-\;>8ye 'gƇѫjq;bj+rh„i<:o`摨Ppo *W<TܽV( كvPPH& 8@\wЯ/ Rfh -|n/rD3wGm7p#1+nH%.^mgho -܈SCq|'[Y`΍e#$}7e}G-&8TҋwTSþgbC48c\Aܷ9ل_&#)'h[(]>o&x`)?3AclSm27x@(TG@+i%~7MM B /r~;%0IwRWyfn;#MPܠPN|ORoK M^!yC V0āM>%y6=jw?W#=ɯ$IhAD/9ו䒍< p1rx)K|kKaN$eeL}?NRz/߄FA5Λ8CG(/#j}-pa{DߞU7eL@^4HK\\Y`4%=zokc`L {beq] Kpm v^VsJ@+.^<ЕVyqH!4(A~'xVMLMt)+̠U+P @(l'hhmx>Oq8g֘: ͐M! ΰ >Ŗs xs*bS-㸛G\ D*Q/}Q_qw8n*(nؽ$:qW8JVAo_g^i_Jy9nPv@0삾i6ii_Ȗ?-U"Ók%*EPe4 Tr'W5Ry-N 'f'P.Z zR8͡<+n> ``YqHXn@19 A˙'=ލ@7"ryʖ_űy:}Ċ+Y:eҖX~:נVeB>Es`sQKI'KҮbQEr`yPVs+Yp.DBgЌ753 -Y6$MVRwƛ'SGUsx3u2u>b{N,㫍Y+&˝bŹ׭5oOM-'Rcu}'΍|8zȭ+o vW__ah_R=KfA9e/%K;2v3❠/SuZmV zWfM2F dT^sљy~J0@zf}A//+QN FJp7ޛY(*^urce>~Sa>9\|I |5!>K~Gg7mߟ8dHmź82#)пV:zlJ 9?d&tr{%;t^|5&.Ud ?_Mw;?>{hdb,N~gc@ZL\Kh}=YB C=Oߟl Z9^A֛{d Дbh=v#?csDӹZ>?_o `dچd\Yمv80OGnTq93{27yfWٿqUȦŴr*nr@O9?ɏ.^?Mjѻ` #q- Ơ6Ďb?V-U7wU?Or:/1ƚtYŹ4ӸrcIN"&xCfiE_eJYSI4XJd}^I _?tD(Ӹ&L@. . PT\Pڪhm-իRWjۏKVZ-njqE\PQAe-,A K x}y=Ѿ3>sr}朙9uҐ\ 3Z;Gq5>Fe$j*. X?*1*dðu]@ a2yGO\::'7 8O ҁ!}`H&65Wb|NW|4ވdaF(Y@0aģY 5VlA㕮kXB$>!_Yk} vZ&5_~ EVӅo_MÈ˳P>Bt@Ba_nk'،RDܡ5UO|a!P`u&@I)|]%/zBHQTW r1;}~]\B| !'bP2HZN_GdW;@&"N8%ļK8~<=A`q!d)=sF8 Իe?@` @ABpt]C zl. X=װ.SjAsy8 ]v4^Ǟ| vRf_l 58.["hz¹llq<4dIq=«.#$ Y~`Xs8 +ip달POx8\4Yc s?Yq@==q|cףz~8p"to4:&dtH |DO§"M`)OlS]0aS;C^niwoF6=VރO'tШ LG?T?Vcv2@p14c`&`:#j`À`ľ@& } `u`{⾡!}C'/$Op % c.,-t辡!}| ,( 00HGM|W+?19`Ձ!s#.=Q KpP'#MLKZE%^ eK?h}4w²^`@+qh^_;X2M,ՕegqDز: ˅`řtbny#̎>xc&nlM<߁K?bs*4ʯ:~ 4yrcd:E86?^/[?2<+> , EQ"2бVM;?I_:l9h9.ƎG?q*o~d~00rI: 87z饗^z饗^z饗^z饗^z饗^z饗^z饗^z饗^z饗^:#_x8fq_?l)|H qx;̶O0څ|hJj?Nt9 ?0s?tSM@i?Fso<aՌ,4] $B=-p( uۏĹD)֞AUd: u$jJLDaiu<{,GOFb/D([W#FbSW)x-wCTg0h""E:%Є8*I"ҕAX#xXeoC$Q珁=ali)ȯթ GNn?%)S 86n so]Fi$M+ۓ]A%h[7.EֽIzZ\G;$iM[]6 fwG7lnDu3hZ^Djv N&Ba̔^2[w!nlgҴQy4@ffKzw+'vQ$&b=Ra75i#$&-AşHzt%. s٢(^xx8lwו?E #qMsG#fM25=- -GbHs|GS8u{ |"OCd$vjG1ۛpCPt}!фHhB#'SӳGw<-Se#' i']O.iߠɣ8J:lJP9)8e5hDO{O6C;կ_}7pt}TqC5y0ૡ\tl۸])0`24e%ìֽ}2Ʉ;`$h탵l,m;IFu..J0c!2ts h~w?ڱU1vdUS' 0` ^`eG_@ |愃w:̗7f#w͉X$1@iJ`#yR \:v'T >)QIjv9(ltfAN;r ˸zff9dkDjlyaȉNS%w^:9@2|-nfdE31 >*:_s1N\  vLU(j(Gz%űTXCl;t azسS D!_qC M!Y0TȻ=k1QxUZ @pqv1Cܿ| `g선py~ GAె/p0Ƹ(4<8zZ: #9` c}~"q =AN<*9n`$6.v0vDl qJYyq c0 uyHyӁ>C^SZb%UMȾl ۦ5`*s-DuҗOn~R&AqE\II4u|ۈABNqD|d9`_'3Rɍ]OjP*XɊ}rcݓyB-ܶn[ӷ18"Tc9 ů)O*i vFx 9ϫ>ČHt9 )QF%Vud̀n]f,C0ːkGϑ9:5)x7]0s%>1xTüf:%_YxO)>MYA8tP9{QɁv䶍^>X k HC!ČIzT0GC;յo?D)>E|+BBWìz| Gr2iGg@`P/F|O~ax ?>dSq!Mt9u q6c4 d]`Os>ߘuv?Ϙg5՜sLكFM3% qvd2Z:~{>!t?=5|3 qe`=tFA3$k/3%i6pG!!x<. ;/C.&.b{S(4g!+=0d j 0o9:υV9SU[XC{ ǽR G H!8ɱXuCuk&n"E@ot=;Y`%XWmr`l%AAH f=.Jm_V÷^lPHQ@FrPʐ\nQ\۸A,%q#Ean=Dud@G×Vex˱4U_ד3l7 2/+Dq9ege{XF:ʋź4D0 ='`PF'|;nMP G>c O @7 BE'K݀IL L{9-M|S ̥(@sǏ'idIݙ:='`%89VVۋ`Dz99r~0&@q"?e:%(g@\ L1ǩ]RY@BZ   @ޭ,$ԩ@Z\VAk֗an7^) N"Ӳ:PVeY_בB{ʗG^|4T$I/m__)zڛBDQg[j;Y^JaofJHrN7alۥ]Yi^.awFCF( }䷕F. r_K>.6vp4#:u8z IJ{qG8x5 BX q?y0rK`@ p +(!WrNN{Lܼz>Nne jܺ^0Kx >K[=za 8Kw w60$PZCuĉ co|^<Ky 7ߕ%W:X] †Bl\JW珢i~J@[WQ%`ac*IIKü+JZR"wӲW p"*qf*U7_AL$U-sJAMd@%~T=>#Y yCߠغ+hٕWN 8{pԐ hɩꈖuʞA3'2'_\'nCM2-[t)#CǪ^z饗^z饗^z饗^z饗^z饗^z饗^z饗^z饗^zGXA ΦUxIO C]I[{gSV)&^Cڏ\g.ʹs:L!t IM[:)Eoצi>)(jJk-4)V&G}#DžsTٴtF CB7t6e*Qy4fz2PW}ѕiH[mCzOR Agg˪ :8<Y0^al̡K&XdIAgUel`P;\!;~1C"ޘ "Sp|,ڏnH,U;-ߔ"dQZ;-( qfqsu\0uh,&띏cI@lS}t\ts[X0Wfw#([҄_  ]P6ʳsGLۀz x.[0qGXtE]\k-#=0Kи j>ol&~֩ Kĸ < 9\ ]=8#g0]0AR-=ֿ%/6,T&ܓj^->Qp;4ץWwЅhk44}O[?7+*w0@rVN3J&`Qq72lDN:4@C`tA5I1`tIR}d;Qv'TIB^l=~~}NzhySuu >xgN;Juޟ<>I9Wouq]Dr'h~.Gнg͝rĦGr;Jf!;Sde c(AS 0r%̎RsmEI7.W|,zmEv 5_^U^$L6_YS%\fejDY#UQ@.Wa69;Ȼ<$LFys3}Yٱph/w^j0zÉƝR}EwYՊ6wt"{ 5G%R<{I09ICqGG]jϮ&!RJ&7_7nKG;;P˱sY'x 4@^Fv5:Ip(yc9v!YS$W\ 톯!(-s|.XY$rly$WRx{EqlQrZ?g3]O^5XaJ8]_sn %w,s"k>I\U5 @UͶ_կ.sqJn+c96َ$'*ij J\bA2{r(= (8/ ngH 4 O  #l6JqyQ|4޳25LXM%.O_x 1ϋ:ve,%͹'{3r#nqe3ZdV5JJ6LHqvuEB݄.G8L`}H\A#j(kNnJ뢼G[=//۷味ǟ+QTxM]0aס~\) cn򯋒p(#(Q]~&N1g7뢸GB54u (gsFA1 U=כ0ȫӫg&'(<*׊\jF٢7+xա<%z%wtՂH}F|k9uYڥmш2UXZr<7|5=L3+h>oi>e8hU2墏Qc q}iɹi?Ϗ'O<yf> % n:=ّg; P>i OK4 HIKh?o?lԡB8.U6?իsEѾAZY!G9|؝0MSTB^uRAӲr-Vfg-)[T(yYKJa7 Z/Qȟd/P*YrFn_aFY]퀝BZRgdQJY~r9[=tVW 3$O!O˺DA+e}?>98_g+gIڋ\z@YP7 FXY{skG1F]=&qo'XЩnu^mS~κ1|jp/W#v\Z8kMt018keŕboS%~l?(: }yo.ǗOKvz|QPV\n]ݛs)Q֏GZ浏 JGs8}Of;$FA7?ieg-yB{1M.(mTJjO|pzk3u|<żTAi:,@w"}|W ((j+ @\oJPIDyu^A;1~\P2T`L`M |A7?d$bI?+]1(YP#T~]aA!YPOp{M1 $:,f]?|gP‰,3R;wm" z! Ef߅ҵW>1($q" [WڑvG~ Wn= ]{dNdAUfuY>gŨ]T2fv?!qTHc"'|HM5I.!\+- YcP &r*@BTzFquoOKyGRډ0τ~u4Jn}{OAi$de6G" P٬^z饗^z饗^z饗^z饗^z饗^z饗^z饗^z饗^z饫:FxDx56PoΦߐ_\?ʀ|A\Uwge:ژx,^1ؘz6a~zi}>/ |Ʀ B f n#5lؔEH _H L=zZ$&߿UO |{r{ig Vx 2;~/SWwaƦFT?i/T蓑K4>&~oPcSUp6-xvnqQRvmD"4]}1U0PtYTR\`<`0#m?l/N#|!dAe|^7œ[=AэRcJG nX;o0ޕBw/,绱񙎅` ?‰[l< || p,VޅnX@ zPO Tubk@77:ۨP5u0&. :.$8|,:|o`?w_gAʒ)NL'<=@l~$gވwbW븒ٽɧ|f[ ?wRM t^ =o40vt\ɶ=:1dq$?2lkYf}kht7 IΤ맥Geޯ\|ۣ J}2hi'^j^w}/hj7XNCgket]SO_>H3ۣ85YI0}Bg` yR AH.%<F:b%|) `sa\ϡ0k N)Dr)F sa)[aj$:X3(>v-Eq}.,,Q֌{@IKQ65 _0@0Fu)X;}.×t7@0M&98v~Uť =$AezO&?(#.;GFz)tUUK<{croKj,A/\wtG׎A k@ƒEj@)u5RůII28ujNRx%L9qH1~oBXZ\C>J5s2$IvG^#_3xN|ds+Kob9*k&ɘ$㷥?"DCܶfE HnrfDwC N8_ F#·Ok{߲ /EN1Cr/M#m1RS 1ɂ92st֛٠9JxT[g5rH.ƹjhl_*A*lm~TaW|emZVBɽ\9T])F\X eѶF;<*\x1 1sԝDʕƟAb\<ŬtG}.;y%J`vB lu1ʪ^r⺓ShCcE:䣩|b?+̎isx y^vw6HYbfG}^Vwrqʕ8}[w|mL/fE^O)W;'0l~9v }oj!FI-U%"t"6^`SN%jDp"ݽ`DT+F `csx AG+Q)B_!R B vrҪD"x X^0Ըw7*Q"B1 zj!A9XHu#2!|j6@QPx ֽ~TeR}'jj>@fϐՔ>}~܃&7P&sfE]~VJ IX7#0@׻+\ z  Ter31~JU N?AY&,@R\/Rá<(K! &bq9eX|g"|#~C{X#_^=9 R_nU nJ|[=y fcJX8b(lt́HUy Jg+4y"{ke\8xn6ӭ4bpRC\ S0'a+z0mten.T͡~rF~Qt},1s0*|hcuFysFOO`κCy %b ̛rՃK)d|g+yht 㔫Ú`O8C:cihbe8'~+տgt4JylAw#38K`&"VB๥xãB^ &й#/4ߡ!PfAЅ0;5a9 ۡ E;9V1?_3Wc/%OWADB !C ' 4gp57;ljRѴكKgyBzt1I$/ҎK/K/K/K/K/K/K/K/K/گ/ xYP={w+q)Þ'=dF00Ɏ=oS(㦘?.`r]F nn?:9  R\VFٓ)69v Щ' uosReQF Xgހg lhL09|8vdZ-ͽu.ݽ!2q`gǎ?ziV^S:p  RzW2a`Ϛ`jnRWwo؀4qgPǎ'A3fw3(&&Iܽ7uܽa~ J|HS)M6nk_My{|{|/F icǷ sn+l;4JJ/0/vij9M09|pl;^i&Sf TQ{}1xl3S)?|)}G|ʶςm}7?MηbؾghgMgGǶ_{U[_o|x{g(Aw/|̍ L|%}tD520p ޓV@ ܽ4o qYѮck~W+|o/{{̐?H~ϗ 4ibn7#N㢧;2aRwY4`ELgs74X8*DEˮfA0{]MqHPRA\Fm/KҀ(`*ۭDZxb#,A>\g_`W :V-"GG.Ҙ> Zit9[w _c fR} @lNZV"L\1?}̙zЂA e3js`EYl{cW A D7g{UFk` >rYBc_ǜM0ZPs'.M5>a[ȽpnzIYĠ-~`?kedP{!_cKL-F1o. 7]ö iceLA[E2̆l&ŀ ]DGӂ-l˺{6~}`-`Zr,3:\v L%F,3iJ>*:}#HKOK*ƩR%M˳:B$؆yF͋cfӳR%M?*:[~A]D=aP"䢮:Z/ogzť(I7{5)M+2x71%98)%f{N"$^n?^0~W,auЉ q.r!j -JJ$?}JIYZ<έԩq7CYf~}vl䏗W ~|YqHih({.֒?-O$&SM_߉ eaW~)%Il[^;hzLWӸW2~Zk/[x9 'O9o]ʲ"ݽ(@,.".fOL:=WU )aNHqx L8$k-뫫,+{ )a8΄8tk1bg3Dd /; ӓGoMJ82q+b-nO*wȸ/5fo/[q2} Vc1\j)8)ၶy>oNٷRo96>a,~¹kDɷsedg[)bN53n>6.m[Ȯ oszhOӆ3WV{3?и Og >  6@l,O>}n!:YU2>q+!_l(6ס}y$]uBt抳Je(歄bCsAaս|Ś>:i!:Y2(bC+s*E ìU ,gʨ8RĆ]Կ[WG'K ^gŊzG$)s'6ޕ~`= J+V4qFKx^3/./_8&]{0|uenQ:meOKf YRzbEcKq~CDnePvl=Nъ:Z*T1;i7/FՈOp=ae/TqG[rGg =yG U\ ]5M[a-ex_eIJ;=:+Cg@ ]A  ŰiGFdFx0~BD+d ]QbF]{^'#Feꚹ˻rPem>_?sG^*wM]9R2:역:D+JүqfB,["䥏_SCMD~7חk1>.AߑZفB+rү$>|(bխ?N)Sf<9päU"Z$` 7yz  ~w 픪Hq>˳wYN*s~xѹ@V\^?#XV]Jݯ ީet!7Air&ʫJ9__8{G!ehfNn ͝|%a`cd'r䴃_SuUEo8_ ~;U>٣WɋKc&{T_l2.HP.(d~rxmr%|y6Yp1H*5#둓XWv*rŜ^_><,ctH˥4}^0Lwy.SQTVpz2lr-å@_ϧe鱚ҠsGMyUn)g&.IvT#f(n>xv53$A;yt<- `jZd=|4Y~iMFfACf=T8d3 :cH<`kA}}[So3! Y$\CEm IW[YV4rl|L'NKR̐tueKIʇ}8Lf.o3S%]˲II3V6 9/MiVg$]ʲKN(/w?TqmgcP6k,릔w|lg+HnwOܻ5:xdw %ed }'2v [%7[ٝ)ibL{V`:=-Ȇʷ3$n.;RRCvKg0Ff +4е*^Oy&%\C+V &ۀ@ JS$g$nCewKN!CiNj>昬M3ӄ1(%MW?{p̳7@Z 4гJqf?g2uX}&t4]gkalۘo\zBG8aF'JN%8칚Ouw1ȹ+!5+t긙shك34l[$6&́"!j!@a#' ycQɡ-&ar"'~OFe6†O@Ԝo4aN-rj%{.0LDž'hc-vKyejr#[>nmτ98"aMy"5~O|}N؈~|L8vL=a66|x[\I2'$]-zz'̉qEB^MyLmr+F eܱ ?6s?76?aoH()ϕT߳Pm=lX>gf]#UWJ:P؋ `j!)AM mGfb D'6HڊMVt0dX5bIIN:ϰ6|'mܭyڦmfNև8EfrIIFuCƅM'iɓc-P1uoIig`n 9 k%%k 0M ڏOҒ' ǶQ}&TtcI2>/2HJįVXrCVM*ɓcp'P(2JJůYYX^z饗^z饗^z饗^z饗^z饗^z饗^z饗^z饗^z26|gCq6cy M[&~> 8al=H.:1Yr·޶Osݺjڽʤӗ67|2Q.Zu"e-M-ĭG_|̴`++Y(omߧ'47X|m|sxp)ŧ|w.PN^< 8ů'}zmXUL{{ 9fБ/=_UMtsxg04oEk,Nw3{|AG~X| .hV|/ޘkБ#Ows`og\_ ˗v\!LG?9/_Da lb}_58j11CMC0f;Bc1e|/_ ia(bi>m̷ ۱8a[2sſ"[z}R,mw ۱3!OؙIۑoW`E0!S(ro[\[q5*=cGg=!Tv-q쓧U$d~9iuQ8?ɩ"w6E?nKM/9Rnۦ8@=r6.)N:UԦ8ǰVퟱ\9XI休JN/rwiK\vscۼ׭/`Hȧmq^C🂬Mhmz…^E>6 yuP0v?lVٮV]'\O/i'uUO\" {/ŃŅ ]e\M_6}\6x"yRWtٚkT$Gc{8G-}'\?U8Oꊟ.}y48+)Z9RE>鲑WWH~`q+.=/_ Mi@ f׀wc`жu֑%F 4Ԧi)a0f4b&8#>TciH03aqMlSk>Jֶ-Hy<kNF <vא#8:GMm^]cxCǖ @cO7l)WecbNTVuώ=tq~ q=.uu6y86~y&Uc'nB3๓rdV_86/t_Ɩ!߹VWcH<mq%*^C1nk˟cI*Sfc{^J@W?mo}`V?Ҫϥ*$TY^ HB܍?VP]L> שke1^?Q LVoϛ߼~^@L%V-/sK<ل_y 52+M/R.WnNfnV >8^B=hx? 9immHCE~K)!6&,Du= !Hۇ:PBfl87ᗽDuB| pإ vxf4lmDJ}$78a87j^¤yZH;ϊa]' [~6y7ndjl2DTvn9'1;L?\{n_(qod+َWUKE; fSBV9&ܷ8)ᘺoኯʘ_1oX,4jyl:ߨsb 4}C7[ͭcoN8ָh)nq瘣 4}HCQW?UxtEsKr[:〝 |&i}`r8*cּ| !i'텈d&VF.X<vY2\Kf/H8.8+G5REͥZ9~ ;ظTJ©ʴ]!>%yY]tlNdv~c؟şlYhӖw&84nc}BBǯ% ]a W`.츻KCʶ_9yfGJgh,nQ$qqvi{?h`O+j ;tPꐏ&s\8~vEG+z-|_Yb#~KvMםg}gpT5'n4Zi7ࣳvZD1€ض>Pj]kS>rǑ%x.趭H[ A'MѐE|+_kqQ2Wql' fc).MuQڞ `3G))ưsKlfyvoTʒmy93^R)ien&mx.#4㑎5/f_PÛ Ǭе6usջuƜ%y,owݷ ʪ{b`]&ct,..mmZ;3mX}X{gno)nշZw8TX: nuKB *<΅#3 g!;Fp_/& &#i*?9G7j?A-e坙 |g,u{gmڽ"xV78֦蠟=D8jwH.0ުݦt>Ge}/" ?$f4em5P7M}>ADd'IR?dK"OZIq_%x}&ϟE=̹o^Iv1[u]/\5O`Q[:4w~OoIL@P*s ޏ5qAax!<֧b?Қv7[~V6ÄpC 5!>̾XvN_5MՄ,Q'VMXRQ(m1pk/[JI+.f$l~Qś4a-ĩ)(eG\,W3ϺIk3a<aMA(5>"Pb:)vFw,;u].zy'qEx53 -Az͛3ʂ}F^?9Nš\IѡҖ{~ytkmo`B*٣&<ďD8#h(PVjB@!KdϪKrGfJZ&qbh)p d95咼 %` ^'Z~ 6t瓴c3-ho:Ah(7-Ə/9%`WK4-[CoIZ>e 1BȞW[ ;?r[́Ʈl Mx| 9"{aMn$Pȕ0.w.ZÙ54Q0/?m˕)UҲ KJ\PM@`\ۈZ`! n XU 9Ƣ+r £$YZN 8U(#c.'2 B<Ѕt<3W(|s P,F&"#h[X]31pm.B/K/K/K/K/K/K/K/K/7jǼq i&2c0? dK{z5C|C`;6K ޣqx&n 0~SL88s.~)FOԽZD1 3sքÍ _LEnQym\Dom>kLҘ) s="^߬wfoo?kL나tc z7^*h&a&\Ð#}'egwxvcS Q&-[廏4:3As)ŬzqӁ݌Oޣϴs4mnlbU^;| ' ~u'@at0f:#f24 C̙7#0'Q!PxBd_@<ӹ4fCJnFIVeDV=agkT_kKZۏ2ӹ fÁnsf &U/Փy ^g<{;Yee}tc6llw3c`2|"I'=/7l+DLh}tΙ1g|048o <1r[(`Av|smf3oli·873gPӖ 0 싘L `r\[/1 TbF\~o>3Li &|n|a@V=Y/;7 #p3m%`a^j<Wc~p @fi[:2 #~3:LL _Y 0` 6> Qp0mhOABiXim 6MA yB:*pd!,!@0LNYfr|Qs"\6}ژ(zZejc&pe:i[TÖ6j]\fo?e[X&,iW-2a8 Of727 ! kiQ#|o^h*ш6ߜڂ lދM60F!]K` #3hIUuԖAi'~tS,!@M:e[3/T ߛj@B%`XO-@Sն6! %MH NYㅠiܿqWö3*T6CͲ ;  ,Qg ֗n;Sф_WQ *t|gS܄/SǨ <JhejmaY]ƶ3rk4|G?iȿ&:@r\l䎚=֦ul¯$ź 6ںc a-ؗ:Q6FZ헤MAYW!UĆx&Ux0dVQ'Xl/m?JiT N6ě嚻ZZVRMYpH)HkiҌm,$5 "XەI+Ӵ,3In/Ek~wSd?jp餆y´l\"c,suh{^_asεW--Be47pޏr, 9ڞn[w9ÜpE PUԪcnbCV )VJCr.ظM7c[[wje!m9e5<9`@[4\&c\%binӇyB[| ;[!s¯E?AFZ$6: 62Q=f[+NG`ZE?+!5u:)U!(Ž%bzw+`ҽtU?Gx/w[vk/HUawߴ mOp]ݦGaUȣSŽi8SKRU-<v|M-1: #bc:쒥ʛy(țGcz)s~b!%ƸyY73D?$+?}ccU\(lś~1Ca=WIZG"ׯ ۸cxMF}y7s-z%rkn:.R'ʢ͇W~5ܫ0\~B'\ʴ026n') [W] f} C W1~s(dqhV DR.D|fa_o((גZ,W,8 ."G C̀FQeg**ӿX4q!Z^bbYpXQA⚜M3r؝P)heQޭuo2iWۂÉ Z"tbyP?gBA+ήP[f38N覮# S[+#湲 -8QAMs}̐+hyQu/-[?"ƕŷ.kl-G =ugFm\DA\YB 0*([\shŕ9JP쀩C2--[.C=dyc?Rʎ\~S,D6ρ+QSJmJB<곿vםgb+;_ޫab{ﲗ6 !!}&[q|?~@IĦbQ 0oCn>9>g)7x, yu_foֆД}ඥ 6 \?>qy2BJe~q2>2-.!}ic'%$<;}}'Ѿ*2ܖИ?a#:qtbw5x#b/Z紡~'ka o=( <Ѧkn| tߴ?0[Ʒsi|x)ɰ9¢_,yԷ3ۯy?K?YzLnA? I6_zV{) #֫'Ϋ΍w>+tZ|Pށ''W ttw.ƛ5Wm^OEHぁT9m7w\=^msY}9p1_nh"oqʩ=֪erY=–>3c߹u`q TZ֠pGF^*NKsmk燬\mO~!2Sẋ Tk.o1U.ud\/&f}l'L|+oBt! e+'>ŌiGfV-'8SO=T ޏ>~BALWd4vx۶:y ݦx m]o3'}z6qZ_۶hz|MF ޟ֐:jVE^ض/rs>&asNFCgjѬƶ;ݜDF>P xPWWuIqqp<#OXP ƿElm{VR9 e!83_w:>ڶN?o˼j%-0>f Q}axmTTTTcnCd>|_w6/&1g-N/E]e6mO϶w|:m߾:Z*3/;"v)_$x6qfX:I-0|z[um~q2tӳdB MW߿qQuȦ&)s1OL.V)nd4s'%keW3ybur#$J^?0I]PK|9'H.7I'iV6[|W !}x{nCf|>]i ?VeJ:9M hcU7Ւ=4^5;^a:d*GOKobyZeU|t!z|{ygVVnHd~`>vhC5KZ-ֿVUᏗXv9`wi٭1KVۚwYUᏗ_-YC!}-??[#mҘ~Z>[3UU'xӒ?,';IԺk6lUգ?^]z{ҵ7&xn+mnJ˛KVjn` ʲgSz푦Ȕo(A_b/7iZCO|/dĄwɉ2*#ARLQ.^謋t0r}f}C&Sw&lb0 P;cؼ0;fC ǞQUtʡojW[VD&Ppkx K# ֜GZ.E0 sVp .D'/p1ww -1=I_#N,٨x7%|45(lțu-V00K>lb֢o:abho%?Mքz+ 7,lZx!H&%7n^!{4YS-V0B~KiKb6&3ܫ߽ }| %f&(3x6̙SW` -|G6hm?zʬGC _S״vo yXea7ag2Z/=ؙnG얅^6o_ldvSHރ{Z–llEu92ZY49נg=xaպ[}ٌFLd|n(֝ÖzbYwVwq({0y5Df5szz]w.tF'B)1I_ 󫰫Ƽ’# طPe#(> }WiٴUʂmz9 tm,?2=n7 zw^{}LxhʪZ$c*B@zO 벰e?C1]:—$g[V8Դa[/.HoUp:T%)Ѫs9Vm2 U9¡esh-+H>ocPuF;E7oNK}/.Hݷp:')+^(ib(1VUe;Bn/ZY-/K^lGȥE+kSU潩%~Bv_'8碕jǚ'V n|nopmRJ^qtK{>=_+%gj|}c}o y)(څ6yw؎Y}qIqYZ)MYNw{Un>^X/#6C̫ o.f"ٕTC "z/i6ge/˯%NCl_T}|{{aY^֥:Q= 3l p)a{-n7jݼy&!z~pM^Ď`P?J&9!22WP}R|S{!2׼Z}wj~bxDXP. JM*rj ӐAA=*ow(IY^2K5u,6KdjmyYR!p7ySER}M? \VIYM[x{rq5d[9Q[K26Q~Z ƊKR7oO݌~JYu9ngZ|4篏q5[yQ[/KlZ>]}M'~_6Zo\s5ZQ[%6Yz\KMʇ)NA\Mir){'@`ϷxzgYNr6Ed|*Z%uZZiW>”uj(cS)H_G1Yn~U/i٢-KHZ|7Z^ -L NŊR7EHA{uF?,:v&OZ8ec\| S^ԩ 3"HA!,:~|y7KQKS4iK~L¨SC"VHAc?|u*rf_,e0hgLAK..Z#CCpQ!æȑ4JX -EԁB%+~8/gȌ Hc=92)$k͵ѥ-槆 KyF^̑4zT?GJQ?qm:責ߦYBy`?ZX O*44+K42>GҸ39r(Ey3$_Ȗe5Oq-,yQ/l-G} )Eik#c#~&+{~Ss -,Qy6\#i=yuȵa#Y7fѪcw_c^z饗^z饗^z饗^z饗^z饗^z饗^z饗^z饗^z?P;2p  \kg QtC|p Fl4-( (wj2>SQ0{. O*.x'NC=|666{-QgamJ F9\-pǦhd|,DD3iڄ>Z=hܞ0 LdYzϯ qC},"i 6½mGqK!|Q|(hdCT[7m]:hJc݂OGI̺wTv` W |IDATQo".ֵηm]0 >ʮ.[̀î:̥Z-^>1k'F)-Ϻ6*D 0pW{Vm24):sLἓ"íuYn,(ͮ?kOwh۳489nM~jd_}=7{|k|##7&՘8|DE  xk0Y08]o<8ڹt>'ɚgxL&Y}YOMgهOv.@ГQ_D;M0e&u^| NFMiό\~rp_sei蹜o;־m'M_@ i:&8 fہ@mkx U'Q%<q~ O[R B|K9bJhBP,\ (l㫐8:~GB)FP_@B֫QSOc/3 W ;>2òhp۶b,'`ۗ!cT |N~Ћ&6 GU6|P#y)`t-[[oޫ*;E|D_{:>:?YC}~Wg{uVWX]7 NbPr|k̳R) M'{Ō";\KlumU)O,cJ4b?iuY~A:'Y:7|7 >ĢQ+vٷ@Y,wϫӭN Wm=s̩FHo-AY#7 _ZKc1jݳ- 3Q'bVQgď@V5Z.`oNҁ8WK7fJ~b/h̿7&Z'=GTUҒOȀ9ڿnWޢ1?yLt~:Ə(zǐfN,Ua[mU)Om󏎉>ώ_%1{^ڱ>XGr'~л=65;@ Ђ/FzT4 P%KǷg}=}BJk.A4B\Tk[ߚ߄?P rK\Ma.ȿ Ecz!tOٚQh/-+P<@rTh]FsB?ekGg93}}NȤgdiGT6v%q 2ˮ6GȺ gddZh Kr'wOˠ@@dBh ia/IV̷ e[b/MD9,Ou0 ԖJ/KąsaX|V aos? lhqP-n4Sk"] H ŹјfΟ3|D$+e^F$3ͤk=>ھ ,4YЕ^"C=i&誳׺}Ljo|#Ȗ{ X{ni&#]Vicך"X ;3-ԂO,G\aݾa$B$,-ɭ%;9 եfY𢡄$F:{X@}gK>j>wtk޼"3o-d֛!F.2f0';ck}oލh3nſus}XA70>\56) AY7ODB8UGT+|oM~|L_/Q{0,0e8$bOŖ5Xv:UAi3! ]Ī/ Er{Aeơ5sWXNvf뇄=0S?2Z$ea׻ w5k-i]vrMwOoo,m!S7CdXO.xO2(_>ټU+8Ws EN4ev>۪ ꁮStӜ#i.Kw: kUs;J׼=0Ī;A`_`3 =Wi'G]mdGC%=B),I}Q_'`ڴB\W~>%Y%4mHr)P< "  t!$R .,G y(BUOae~?ꎩѸw_"ÓxH*ͧl?-ȷPE2d#G e9=l zpM&q3uȓ!-OPSiX~%s-Wy%dJB ){FkvJU^))Ks+;.ǯޗ޻ *R T"PF454&QoEM{Kkbo5H)KX"l>k{Ϟ93̹ͫdʠCp-úq=p+EIqMCj(o)s[g<}ӌ|Hڠ()ix~8#JśɔO眼lCm>'pGpuzWv/z#1?U|@A9Gݼ.5uzt_Vaq/䵼&b8WP`6s诌Kr[u2}I: I3{{X- [prl'CQ}Q {@ѡnC]IZ/;: vl(~.@" (*2>|~R LYwB 94 +P-u8AB2ĪV!a%?r] @` bAy 77b(L٭{I]/FX&삿U' 5x1 \1(.UH~zC?c.ɥ*×w˺UPP^pzibݥ*9Q:H\#|.a .\w3PL,T6 E` zu74_нꭊ=h]vROH k3L/^wVſ7Cj*nX "__xWgj&J (`IzJޤ-Бoֽ:V;eImǑ^Mq<|~ݫj}B̝ @ FVdzk(ÀuF-`g:""- q@\:dmă33kSFyg ~ åD \:<ۈgPKFr DE`7pXC\:ۈϠlFP7ա.GíLϰzG`$!gð%(9fn.@.oĺ3ayA 2kȊQaX/߀.@7bT`al8|Sןxq`-BQ9J+v 7oPW18yc-V]KT=i uYcQeKz?ʋóqa.%6 6xraƘgTO-bq:qhESNj>qZE7.VnB]1)ڀT=M+n'l_2K{98k"e"8.,꒚?'{_"8ϣ;u=s6Ӌ/ 7¢ bK>K>`äI>|R{d멺1bĈ#F1bĈ#F1bĈ#F1bĈ#F1b)3kI"{sl%%5 K6(sW^~v9;WI[[G'5R~ݢ$碑3X_Έ܄K#G 4У}&ߎI:ܿ5\ϐF>7B؛Qn% vQች[RBֹK/1pAӆc-gم} t?̒:~+6M;.?i (]=Ct8uyRyz ~&>!p2_;.\Φul=C}t8 I䦔K1rA"R2tņ~I=<=[VqYwDĥ9&f$X14 2&)k>!yr5i@⤤$s\|-b'<~cM7<ͻ> m)}:nٕ ?8K76Фv{.):fD'3t˵|R}z n܋99;t˫6%'S.xs,r`4ʼn޼yhiX&=rn ,[ yC[`"m&X&n誫qLLBI*ߚB؛yض_Cae}F/m56![vfz(]̽ˎGGx!f__'"zif3Pz[K ,{/uƙh0cuA`DX+ܰiȲ@=5{Yن@فo4kӧE:WqæiX36-%Gɶ9dk6rӚl:6Y8!zO뾞e9ce!@Sy6[87zO뱁0ceEa0p6lӎb !~:> / zl`9ؔ .ȉXJf(` xmWu!&#V뱁';7!Vyb< ~crCe=o'G-&HMc?7bE;nl_cxɰeݹ)iqŜӋ`Vdž5&Ozl`[8}YwAJn=(!>́6O0q M}vq"4FjF חMzl`;8 nʉ\?ikQ_;Bsɷ>@Rlòo56Nĵ+pcV<@cE_z$tR qCo_!6~ۋF<(5{x'|֓G~ͨVצꋋF<(?{xד'yGPS]Ub|D\(/qFG(H˫#kqRv#e]s%3tJ|`K퇢E\o㨬y]LY)m OQ}RVyZ&߈ή9$%T2ne8*^SBSJȿx~h6,]̲6P$@XRuɹe H.#PGU^\<7zQ.f[)R` ,OQY7H#AAv<ʰ8)6 eg#i׋Gn?ʺoHk[.E O?poAEn/Pֶ|`H6 Ej+u}mYe˟7"%HiKlN<^qyòP@Z145/.8^a[)B` -o?ӫiɕ;nCtܙ`'"mȉBz2\)PqFq-T뀒׋=G/nͧd9QUW.ջMlg[Iey5i6lr{66RNt?}wKsVg.S~8jبmnȯ+ޞ) ؐTD84%1+Qכw@k e{ȓVmmay'@fҋbˌYȭ}\:4Y*ONśJhPI>-?E~%7J( WYp.rcG x[r?|Vby*ndۅ:'M"_Ѐg%AftqOѠl߀sFez ىW?2誒7g=jޝLmv=T&ēvʩ0~eGf=RilJe|d2"ӇV+?_tnG]\ryG/e8ltë7tPj[#F->:4ߛܲ=4:YTSÀ,ncGvy (0EHx0 nxslgm/$hyVAZx2_j, w48$6:Uc~:Rs1$e]ߐ󎏣{r>$A~ G Ga97ԓ68*3mAm2|Zޖz_:~?غOogR'hOGlW M_~^uBsgl9>IK nZxQݟ"Cv9;hR3 ? 8;5p|ES:uy}uq#<2[03I]9ޮG.I{[ǡqN!H6e7w1C~ꉛ~P>ӆ)<ςZe9oX|VA(>&y]0n^:4ߛog\# ƭ vRO] ;QnO73NCFR+k+`"z*3#onEЩc6/R+ޣ/ O@ɡjQ(̭;$nPd !(?wF4ߛ/BXʖg%nXY ?Y3aME,rhɧkJNr~CuN.-,g-}fv3!4gK|453;󪂜I^%n֟HJ8~J/?;o{qQY+Z]4TW{cdGFr=gh;}{ãVv34(Ҋk]XM+Oyi %J̺w8+pS 8r#>b|w׽6+s@pF¦k7_vtAFf(,n/ X76@>RTC ߢ8*?OޣѻxVy%2q"lX_ԕn?;0S7[wΉ7(bگ^|LV(->BͿ|es)޾(*@i!V7>{X.lNg WZ^B10-luwelFV>ǛY{X)p%Y qa/%g¸e8u4xS_YG4لKXyY/ED E7VN(τqhZO~;R^#;"bPbm,.*)-<zKb-@LذlPGNM+nFgW4DU> _)Pfm,+*(-<zEb-@LؿlP7NM+_Ur楋qj+w$S%rPH(ò pjA"[omH+_bdGp-8E9她iaS3 9tavi/}جSqJMn*v7K³(Js¦^J(ϼq˂84]"SSUC?aU/rm\xb[ľ^(4FÉ%7wY 㩍XuہVyI$⢜ܳaS$Vϼ ˂:rjif䷫VW/BVQ{0lj2YP7N-MS+iAkۿN]{♎WE(B$Voh%nG`m8i-LH/T;<↷cm)(/J XHSʪwFM1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bb؆_T>ϟߺ)ЖM>ˣ_~wwhz|WR|yu.E&>iKi^=ЎzlaxTٚoo]ԅyWm3g6eǃ4zURytkyu/<%o|@I'ѪO:}4\ZLJ)k6`"6K(l[|6p$L?CF"[0%K^ڢ!Dx"pqp]4ˇaU ^o_\s\HÃBup#aR55sAQ=ѴM焝gגFxE#EG#m, 1Q_Nxqxl5<=:mlHa`E99~ xLθq|;7IiAAc_=61)YN/oL|E}>zG'*vrA9< ls?9)nKw]s ic(j??33I-0\FЅwJ{>:%m0pDQq 8'g\3yM|ly$n\7M[6/:@No>Ag/?0q{&ttQHဣK/dzl[lE+]'qYh2J)q;)}ύo96Pw^2ށOnabnV4ctȆGDG柿vk>A{Ί{`گ x_n{[ @ * 8 Q=ѱ!?̗ raٽ܏]䧷A,l2qhP E#?晠.uH;ڃ.>rߕ-ȭ=BacB͹8LO' eva&\$Ae'`oB)W TZÝf~eBleU&UiohWtOqWlXQiCY_uJʪmmH.*Ӯƞzɱ Һ{ݶyp6Ʀ>o*k­O.R׮0zOqWͱv wI[íK.RH]{k ⋌'O,Hͨz]2upkcjWaOq5W<бXԟޫ޻Agㆼ \c2R235nyoBy(ӵ]~]^lU%)c٨yg?Ln~nDy ޖ\c2Ґeglʓ˯ɋ- ,.Ӵ]G6nY; ؀S0K+Qm ; !3kU@)z9fݞrQav p j~\(W#Q=~]6Jn3!cL[\$}9 C ˶, 'NXOoX( N΃-}=V('ꌝ .>oXu - x$ԙ3UG6}_2#{KD#}=ƇUO(KA~r(ԙ3!)cbr#$! &[>nkGxX* $u9iۙ cMz]4c^ėVqz: d]3gµ<-('d@ϽOG_|1l,C^=5!q,z u&C;Nq\W{Ӌ7c/2#lbc]2"V44kBܝ19.M2#7n/}wjEOY>ỗ|tY}fd1;_\:ulPFԊe6ữ_/^2#oΛEکc2"VOb]fc?K`)px$h<)z9ׇ/qs):G$E"-Cx2iG<WH Wb.mSpfZPi$3 0ȁ%Oi8ļf˅qsa>c]nɟn*>/1H,BwKb]n_{o* ,Gr@X[[\|N'e 2 QՒC`|~ ]VW'Wd @'cћf]z۶.Ovr1Wq6}퐒:Ը][N3Fy:ipؠ,N[Z4faғ"\s幻~|V'MEyܱ_?aC'6:wwۿ55:c(Ϗtm5*׊ [3ؽO=0A˸Z\|N'׸Q4*w쮦܀5vڇd$76TQ6+fo,_I$[9w |V xC-\K/0n+PX箶ᶂe5od荷Vr;LxI8wÍ ~(;+n?@M@ `9@'[ <SRذ^7 aYK ᥊O͇3(e._V{Fk%FnE*\ (ÓbX ݆E8ޚ_ A1l1kh䋰=DvÕ%ʲQL"+j2WfNՋf.LJWXZe5/U|+uP@PEvN>koHϊSXv.~ULʂd>=# xG[/=NI-K仭Sw[ӳn(,ʲ_]j2/Or `q&u~SFGGnvËuYgʲ]d9W%XNQ % )YeIk2S7Pҧ  +RdpP!~30\(@P ,twp`i!3JUqe.3_ )@\3 4.b\ e%LǸXh/#nE!8 7c@P\D\4KS}qCO*W\S80p1Jv!^ nw 8ju,:BJ6 =5gnd2spDpUi.\AԖ`A6EbQfD|W $i!Ob U|"bkOnHzI\NwR^KR%w1> Wb_靐}NwW7^{jK΍cygG6A+ MV)|Rq7uװؗ:CO'wk ;|.fJEwNRDSdc[E7OLUϒB ux |Gx 7h"s]DN9]6kױ8- _|ǎ 7͞h"sE^ԒϱM7֌i. ?|Dž 7h"sFl]Ұ.~/# t`ka;Rv+ 7h"~>݊Ox9%*uT)A_n H“h#)"=tȊn x~at͐“}&i`2'N"z~!,pv!cS}Ʈ6߄"X҅s&f| KYS F|KcI\ >!KS58i[=^jlĮ9߄EYhGejq Jyfk 45M] >!RuVok4ȷt5T8T}}kK[ d<6}mLG?:‘:Y)M{{AT'|ldu_/=؈OKun ]:Y?l x" %$>];܉7u5WC|=iXK7YD#xg^2mm=e"T&V8vqzk`Q@uret^^v~izDؐq;`['ʾ]֍=\'njJ o+\a}k7yُ_JS\#??mY=g"K~]ߞ5tw8k$-@LɯX'oٻl(ίHӅxv]c]\ a"Ar׳cvQ6X&*rk,29R=y׋lg҄z$DҐΟʗ6;Q> Zhۄ܊z$DRΟʸ;1OHQ￴ܲz$DR·ۓξn@YYr4H~J.'!)0&?Nݻd^E-MW=9_ko ɉ)n?vqؓ5cQ|ʄ,u#W&'ViOWtMiB7$'}P\3mB''*Q__b+vL3@A+9_D7v@>T#QS3W|c'}yJII7kWy|-2;ھ \~[gZәӝoݜ5 !氒oeȍ!Gn>?Q>XX鷵OMvZeʖu1ǢDY+}a-rB'`Na /@r +~AGwϷ~߸FϷi'Nmڛ>c_?D'Z|sM-=`8e G0z!/M0!~#eI5Ggq&G˻7u8dMK|B#ewtM-=zݏSrxLb_%E|GyF/{7;) 啅D? 9przhw":ČxMI[t׿<^֧G{nM?.¶ _C\11z7Oѵ =.5OinU]-t^o'ˇn땿q=SWjNyiuha?EA/{V` gm3 ~zͿzQT'7S6DFo޾Qj{n/_gs5AB {wXf,#mŋ#vT>8F?`vQ@x|k$en_oIa`xsїt)z闠胲(<5WE?d_=}ZgmiVf #FX=H/j>$ALHkԾ)p=aA@I!*ln=2< -9lSgvԩ1^)됿EI&6Q_RtkwSPP`32uf/iM@iQ>|_P(B.`uqق7&yoH/R7xs=m_ * 7BikL9NZS/p=pM{*A&.T(φDm\^#ԙӥ5e\Q =WA&+Td(Dx{rjjg+?NktBV' `( -/$H(֞S(RO|W >B1\Q:.lK Uw}/5Z;dU?>I@;b`/,P( BV_@8U/wj -|`nVUmwLXXXP(}=We+=&V0NZ·)wGJŅ9³!q [uѴU/?LV'~3 A#~eƒ!WAQdt?νLÐV.\gA9 ǘk{(PY#8Xp si!SbG%Pݣ̟z_ZΟ\g=Ths>c{PO^]-]r^V31bĈ#F1bĈ#F1bĈ#F1bĈ#F1G_vAu㍢6?7HpRn* Oh4f"M'>I:H( 5 1^G."#/Sp|-hр)^hOjs:%ѓϐr|Gz1)q0d|xZ#,tld!$N #Y`ɇ[:[mtyNׅG>OAa@~BZ :MA捜[CU))i{&sLKKy||Mz=.#F(kj#f o.PC7zzcF@nK]4zL^;U\~GQC|I)N_IxtlOjG 4[;_e<4'_88Köpıŏ um6tuR5JI~"Pg>A;3D3uuR[?-6M}̐Ɔ8O-:<3:,OȎɳtuRѼԟy–Ja/gbC]:?q>360bԎaORz PClxW/͚,691Nk}EW#!EjX&XU4@\'2!EjLU8j77xR_5jHU5j&3rtHr>oR>FɱjN6Qw_TtYƩKUxXP )jPXuԕؾ zjg#~H{$p?rwBQmr:OKRlB>ޞTrq?1#F{46,]qFݑG}#;^ĥAמtc )D$ƻs msfnӗxqz7I5Y<7E (H~߈gY^z'E}#'׆Nh_{.ޙrΞSw!Go'G,1M?g]/F0ȭݴm_z=~)q/*huZ`\澁)R;@@8e=rG$ 8k{/T,讻k"DO N_vh~6| qvS'G;kE |Nц^lwvNn4+[wJ}׮ӆNXNڱEƯ/[^|ޝ/]{ ~C;9;wVt`0N_XeL4]3 )[/1"Ek/NM}ԋo'Iw wrB>Z\Dpx&㵂#n7Y,GE{ 1Nbv>}n6| q0:0Qi=xjm^@LYt/>nm-JMX{.Mq5s`~ iqtQ }/;X+z/Z3c#FV,W7zƛ}bG'ܬ%\p8cz-ngbL8ic`Xp;i~ȷoNVl G3C=مC)sKo>6l k~$[* -{A"C6vƶՊGS1/~pi|c/\`[L:w7clg<ՊOr_1MJ|ѣGk7IϱAKn]QiZlG=~~myhg]x#Fe)JKQZk0)E]LpUo\zReOX˵Hu~_=2^WYtei.rq매Y4Q:3rCJKekC{V?FkG2N,m<'z:V^լ'Cqv]0VO V/4]Yڨ93^\OH3_:hw\jj[n(HO}qcfĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F1bĈ#FU;$Nսxm,CW>?멌FkO[~EfV;>ʞ ^:P_7X m>Ev&<WRFFؿ}tړ^z3bH54MW4]vn `|nVCb_,v4]c:|Ȟ%M?__Nk|I;}xؖ#V4MnH|03_qq*«+Qazt?9ZJ>q`vi 5x~&x=0n_z-Kje>a?(@ɺ ۑ6阌J8?Q*|6vu3#g~&k.#9ç@<[>/}襌j-|g`7.N֍iϑzs򯸷:Z},B8QZ!..R<(p@W :JGYPQOR^y,B5/ on^Qj[xgvMenKOwlOI5-~Gqy9sOueW>nqߐwcv5n0UCCaA#t1dc|=MVtzwp;8y4M2̤LAM ñ14P\x^J}/UMʘIp;N?]HtEAAyK>e4]4qqvM𕶾_[RK *i~8ŒXkSzPV_ܬkx=KKu(iVd+NKWzvhl&%t_cM栰ߞ4M?;1Z5t1 Z}ӔR9݄vhbƁB5kk25W3e357ʂ j>e3vwlNmv~NhL&N:Ȉ-۞˫Eؠ]|y KOU)of2iI7׹4 à t:7ЪEWtCqAJn.ꢵ ܃ߺ55.z#NP]Y3?-PQ'D .4M+DYDh;^Z? BO_mz1b\K?;xaQ}y4rK` >I,^VH^elPVWƭ** ITsʗ6A:J@CyAy.>IU)AʺvoZwo(/(C/:ZlHŢWFEd|S&}GvnQ>l:64q˖G:2"G+.=YH{5l]mӆnDmʎ]2M6c|HHHHHNbD.ꋩ P_2iՑa{tnZ)q%4}n/>KtUIg:/ @X^u=xss3(-ȕuu.|~Mh:dĪ=-k_t}΅YNZ'֤[-Z׳=>[WDW9iGlt핉tѵ&0ʀr~o;'ۡI<3c<5i :k%5nƾeow3hK/iǦBuE4Me<^+)^n-M5q'ڪ蟯i4ķ} Z+Uk|d%4]cPC#F4dMgةy5}U3}2d4ރhʰ'#?]KW\ҖQ-]wg޿ˆ#F:D]gItu͍eo74]z52E!;8.teʙeFN3d4]` Ӥ1?-'_-Mg2ٟ $aF1bĈ#F1bĈ#F1jOq,6"Uu ?fUaZY_k,",Pe,Q9lXFeB6 $o4x$eWg>r;%o?ȗs$c^Vo.c9ŘGxs^46/pϿ{tZ>2ՌOWO\xVҒ/8%P/kàbQYwϝ\'\ˏz ([‘3|R9 $oYRl"?cZŮ X*UD:g}mʩƷ8~r˟Ѓ-ϗ.:?U%w>!Әjs޶6yo?d򄾶"W+}|d;oZTv1C{X{XWe\ŶYѵ?V%Y\.Y^L&`5=8,T.fħ؍u fRdFpfFVV|ϭ,nI퍹Ie=%v<ŗ8TO- 7vUӣN eݍt//9q9: zAoxmQb38(Ru*xteYms*DHeikɕJ l++T4J e rl7LD֦" iʩ훢 NrJhfg\:E:YKy,*?U#`y.[:ً9D}J}ڈ%hQl] l"'9:3tulM]wb,edž^^uO(xmñ&fGo7s1,yyUDV`a,x]53㒔GJg cޝj⯿RȄGW2Zzur@Ʃ~SZO;Rr$v,@ ]]HWB='S*J[QutEoOS+|<)ϳ3«M/ ŝ}Z>W뢂!~ TcO߶+OTݤ0_֭}qe[ŷ_߻RY~lTeia)߯)~ʹkze(!p r`%j-J nQ{S:ϙ,nͬ*݇v9ӿύ^`φ-O{MڦMJ&dD;ktC]MMuuMdsXZlXHU׶ib%,+5Qo-+*-K(\B**s„UWrl(H}2-\`nekj(NNN~WJK,xzDV´,E˫OjQ]G^fn7-J*&{S /9%x}#~r򋦞U׋xtER|cԳ;}L޽ |ym6#f8o=h!ä| >!cƗk;|cWݤc ʉw9 .Zh[=l=Ba7v IקG@%upyuի7p[[  lz-4 $v ~><֛vC8Z;zŢY_FEBI+hK׾)ކɩ7U헣x3PvYvk- Dpm 5yOEnR9vU^i/ҳggCcut~;߾8q3bYSyM"eRS\L*qMD&$q` [Q7?\h-v0-GqƵnH 'v5Mp:tk)x[(RI ~;5QSR>nf<j?߮&m.YFMN r7qG~lڍmod˵h@O#H}QuE"O·2͵#1 DhR5hS'_l^Y]cŒ@-F[E2/;iNbI9k˗{dB'kH՝k~H~f7EB/}""AJ'Zo24z-#8P<[lao* 4iy"[k.$ڒڿy\#rUqϋ-qJ%.JOzMC)Kg'b>ԭ_'u1ttRJ%*LKkHm(MKzݥS@͛gHDȷl/0 8ν:)n}zz2ÈT5xD>*~]ݿohS@k$~F)0RU+Y^p;#s|F)O~!Rh$w6ɶ}_Z[$Σf:fm9AI+}(\uܼ*~MډG_T&Jbn s ]WɷyResj߯'Kji$}g!~w΄JgGN4Vfml럺_ۊ8폤< Ea#F1bĈ#F1bĈ#F1bĈ#F1bĈ#F>Pn@ ç} fqEG~SOkϳſL[P!v16b!ή>Һ-tNW/{D䵖Ol_ ْ|JcilϿ)!46!l_|/ߦ+\wo|ˈC'^d;˄E?qxREϿIGMB#K-h9聖]&}h͗ 4oY6wlצ-5ES֬*ge¢+f.;9`K5?m?>Hy;3`y{;,=uK?!va͸nl\ª/ϪY_{)GMh~V2v݂ϗjoǬl̀-0kgKRx7k^noe|8#FKLH 3>2wڿKcYڰsg>AHz'ڷnښ cm>Ϸ<擆W^`!~[0DRD /c|qVhOȲ-m=Gfu;Z(g~ۼ3?l(軽L{mtҸH:˛z7h+\YEr>m`I,kإG骿4rF,ǻ|kd/2">,/bwk<߿-_S#FS늲*\>7.4r,Vʪ7Ŕ$9RayJcmfn#_&Zt@1wjo˭6otUNosʛ/jk234腬Z5CyW\.녎N(7I]Sh@ duI:6k.Yח)HugG2+tMz$Р.-!_e6|okoYVԦgLj^^{!t3;ͧ(_auW/ǮY-L #xqbі/iqcY^;_c|󊿛h`/_?Y,[ ;cϮ2kstΗ4{2ͺo9~?غ2ٶW2EQ߼Y=T~30x۬'WBS#F+Bӕµ2Yy!_S"%_YleܪM Xsu5-}Z,e# @p\c׮?=m;pS(qro>!2kk4:ˎ:z]mg |=sƎ}[G/8ߎ3z)=fEhC {3:s(/v̏#۔p9][>kVoLBf>Rfu<ǐ g rvp:׃Zۂcº蘰wܻf+QL[\?6N_:jfO2`޻ǘz2}uC4 GXYؑ3Dt揋?h"FJ{%Pc={tΝ;w_eg?{ !pw@ g >3bHQFWGqjk}8eΰކ}ЏQk(~_ਖ਼}K ?ۮYn"Ce:?y)E-$@n:u5[_ `E8Mgו_WԆO,$@I:Oqļ.-9 ؽt%:iui-o9$mb;gkPnSsmyU$%uh#kڇNU}wSK>xҽ=Ye'ls@Jډ)%u.#|qF+;,]5[4r^=]k|ÎiZR;Ը=Tʸg{P.SƩED9w塞uSW-(qא2/E.\Z꟭.w˞\[ǻFN֝XůxŷR-ǯ{[X oKb#Gt۷bh?K/|7o l㎖fQ-V_wbwhUȢc!Έ#F1bĈ#F1bĈ#F1bĈ#F1b]U%tEXtdate:create2018-01-14T03:15:08+01:00Vq/%tEXtdate:modify2018-01-14T03:15:08+01:00',IENDB`humanfriendly-4.18/docs/images/pretty-table.png0000664000175000017500000012600013055423433022115 0ustar peterpeter00000000000000PNG  IHDRuE\sRGB IDATx}wxTUNɔ$@7ubGEl+Dw-[\;TpAĶJQl %$3I2Ifq9s$qoyv1gN?=yA@׎&u1K(e )V @^R^`.@`6`uJI(% d{@R~,.vCA X~I,TJlP8_i!UHl-EKw ;C^ `umrjfK!V[ORF3S̨:TH/o;_HWf֥D>ֵ[1N@@HD g gظ|- 낡UYRn*ZvR]W2\*o %D V 'G2g$ڪBϾ>;Vgdv,Jl͓/lTgh3ld ^|7`6ʹ{sCJ}voNǻtJZe9|YK:N_^NһեeOk ުY-ժ;;3/-rg3 UZ,}jx'+ϓU^bOV'[TnIFV' td'+ϓݦ۽ҒlR94pdlK5m,#p 3KV^)%K^dS{fZ^w߰/""ݦXx u6G̒=BZ@ ި~QcT6֊TuFTe׹Y#z(J#m,Ox˜EIX:@Wр=3ڳ^W]KI.V锦ZFXxe6=Jm;*^ҁ#zfZe7%#zdiC=ҁ#[g Wo B{d&0=37wOe Bt?\bZSYY6VHbͲeR҇F d%9MR O H5~H@bjj! ΋4jTCtv G[FDZk(5>lB)( Aęv YsG\ (~9uHpwK(qPov p &ͻ%e.=DNR<  &šQCÞrvYCAL#H(!"+$^;/ W$l]ش?RtyRlJ9e!)c[m#M[u6;'WшXSJz\Ƞ:jPVcރ)M!>hFʕ!"bb3ԥQ1t*PjJS_5 j]-*xm$9&qgJ>XD1IzJi@(£HI+NJM#A1?"s qqj,uCA0Ed,4jSѐ\Nlٙɕ?P#nD7 ax"=3/B+JZdGHSC{=G4)1WS.PzDO\jF%RZ.t$T!dܝ.6s5`=MC̙"sohBIQ1>؜}T[CM]uU-4-BHT!I0_>6)IeNu\[eP+2϶ԡ!zcfQcYͳh/Hɕ7?f"LDž8nhI iÇFOWgє? :""ǭePI1;h_eE*ȕx_æ/}k_;xYHݧ_M툆V65R5slSr =ex֑" ub]"SO#>\%N0zYlsz_J.)_^}S,NyTGnשw'&%ӂz4uŢkX%wl_<ѠAO-'>_9a(͋K2jpU^}؁e5`bvǥBtIJyVWp-c"໤=J22Ju%_d ɈEZ|!,3wz vwkC pyMB:".:8^qW%쬾,XȎ&KK$_qgBe9OA5^ppUh,l{f?[{lz w:k;]x]T-ʎS*KZHz{@#Wb Y=oTnߕ0EђyZŞBKЪ|]qwͰX v ` .3 qz=@N :.й߇~zY˸@WE]-$% b@Ɗա+{4~P퉏Lcnعz Eͯױ XucȝWhIT6Z]ʆ(~S3ܨ̜Z,D3-Km%ƚ;H/zwK1_qnO<,qvlOi{:|CH0#80eQ3~NɞbNcu;,EK԰UOn`ٞsKla嗏~EJk8|+|'VꧏNNkE' L`cd-L"QUvhz3ic  /_$fv8Zp?sJgGNkd@rtxb{\u+JC@  q WԚH`Qډ69E7UV0uvK[kP+0 ؕPa Ԕ%,`s ^:窱cVW[4(G:RpbU-~uDd3R\ אиoxݵ|WewsΜS\cm ZUQPƩsIj±KuŅrns"aN!v(ftrYe[[? F)҅-pɃQ hS Y}حYgswwtz;(tiӱF;($,خ>Oi6 v@dj!U u_VrMW; hY%M1VC*|ed{Rb*zihD\ꦄYĞo+L8u TV\ PmC48THV[\E]E]BE#V$mH_ܣ_FqNnELS4YRCɬ`6 @|z9 ?q !(%O>wk ؄~+S Y|\y~ʵ MqolHp!q" { 0o_lh?v&A\1/Q5vXޠ( m[ ľ=h#U5vN7UVuY'fnut_m댳>Ǔh]8%)kz8:ɵJ6Rc#řղm68B@D%B6Go,nGA$V[a?Yvx^-AQa;sQOT9u"J7Ȧ=]ݩ9YVG$шU!c3}+{O&@ЮX@\电x5k!/+!v_gS;h:r0u{@t&<ߨVk~_ql(o>y%rNsfN/ܦWRC-)㝸 9ǹ&ڰ;f$[CX)n˾[[ 誘'T~qާO6c|6eiPX;밋4'Xra& Klc<^GS:dSGGJw oƚ?Tw"SqJ| Y}6 ̽ĭ%jk6aH'6Xsl]ъ-ъ-ъ_['N$M3h&9f̾lff^fzkwPo6@Ӂ=G:`w[tSdg3I[[Sr{۬rM {kKѨKJkG'*isg^2ߦ@HagAh )Q<QgovӺ$UV@N敯w,v0gԼ{>sGO_z:|9VO",K#T5 X\^#?,@ z2]>ˍU;1 K4"-Z:"EF55i]η睕{-}{ݟo8Ͷõ %Oe[]oZk}&̷xU|{yv!@bc{njs-hÞ\;)07`s '0nbgQYqG9T&>\r +D͓GAq7w;3ϖ^e˜ X|s^9Uzysl/&oYS"nruISd@0Kl65t ;k^%zo{ǹO֚vb)oF{e7BE`FPY=#OT<݀G߿7eVY~0)t0ge3jِ88E t IDATڔz=k1po i^Wr۸d-`A*x(Q/}=;3Cz|dس^56B3'&X!v? /6z. %wV~\Ƥ.簜1KrRSLujGVWT+O2O2_Sm1׮|&n)#" Ed-8$&2t ?\+M~<8'mPԯɒ>̘W=X8a)T!R q ؇{0gP|S~PԷ뉒^ү" //WWWr%z[!P^Ce{$@hw.-}~m8 -rm*5aE$8u+T>HPKΨܳ[P0Sݾ_Yt届z\t_"H$B_߼MF}d-9ƒ?˞+NR=e42f6\z軕Ѱ7U,=hU֬lk&rve埳]z +6E^cAMWp8튡aÉw>tv{77[L{d΁7=_?xаQVwMJuIišU+FcxÛd02T"0o,Or=P ߸%Xِ,2$9)'An{f[2ڕc._wK9P>>fEOFh[ O{_Pأo/ -Ǒ/{6Z~Ҕ#g4rNq5,U^HV apH{.#:O[Pb\ʻPhQqܮ5Wkzt: |R;8NSLd0@㪛$CYJeǮ;x hӛ ʗ\юdbM ť)J:[bH?V@4&}PQ"ҀV.+-7UUGť@sZXeYhx憒7$Fk4Y6⥲Q32:,M1.| 310 RaFE+1 'z<.4DՕKfV7OMNDM^l6Be&M?P;rW9BvaԩOq ?d>y9tQ&zF]kQziT/C5 âȤF_Qҳ@0*SqɝFi2k"2PlLLNW.)/6wzY9ԬMǠ 76!<5cR(/C +tM\yc8s㫴c5QZn_ʵJL}P>ʃX k"AtKZ!q)XeAG_lzz஄0J)Dm,F%4A$HL(<< qUK&&`#  Mov𕯕d$O{d$ L%_C@ V"{cB Yb\Be."$TB!)YQR&sIf,eT'Q֔SnMfFB)$ R>ĥɔX^r(wtY͊6 .@-&L',@jE=2/͟r.-NbAq9^ SV仔Q2u&Y:|R ȑPFЇ"🊬Hf`ZLA#'%[g^k7ȹ\z"s}ax }p-0"e`&f_3=7H;r/:8iJ!*BFdDzz-MlfHPz%D`4|tiiuH(Oy]4f P0{=IOhSJ(9IBYEAm {MZџM\v/Hv˼nb۰ 7tJ Ν^:0_̭w AZ?W1q^J\ $<4}2S򕩜bd<RSSGSK,-gF2DY'4yY-|IH&X)0FcشQ|E7$<\3L {&H2( [SJE gU c-K*襕#"r g\qLJ EddPgO3ά3\PM_ x"J~@PD+Ѥ {l-{qxG,"16 Hl1aI )AJC-~P-ŨwP"&LRVF+GGmb|)"d ,M/kM=DLmt% =7]RMZD59,fr{0b"0)xŠ-}M FW>k\hͧ28 &hZGoVllRyDZ@(NRpv@yаd.01Td0fݝT2,ͱ6H d<\h S7"=$*z,qf"bvUVNj`3Vy* )bR4@LQf S4j/zFmD4+5qmMFtӝxt ID "ߚ~o dJ%4%"usejZJX8iVQgʤB?mqbX]0XOaX-KScZ(E;@VHڐT7dRXeVCݍW&Y<P, 3r%Qv>5V cF@TW׆2%V 23D.$ꂡ8:dڲpIZńYv/HK2vXY.SqKQ gRV̰kl2WXg+WX .կk4":Z>wn!gsln/ͶVw<ڥrln͓q:41Qzg j=-{|vOFi=o(gkg3j>1$ 3;,ux5K:C×.ni×hz3 L/_trJ,ͺ ,?WF ],?sKK'@KZVތNG̲ DxϺ e@6Nbٳn>!q=,ٝ ΢ kvXV'ecyDxebaT'.Q~ЯX*Qxx eD̒܍wOUĮrLA?m3pd/Ř\Fx:qFEF&WR|.~YF;NQʵ Y.oڥB#zK7T8 3fRuU4`xLY.rꈀRR>hg™dK%D*p 3M׋䘉u2&] e8#TƜ\#zdX.l RzT!^:uYVԯ~㾺h (cU褫T Ԡć-HÜW<4ekJ}0M9a 4Ԥ쟨tvW0Qn4*Oe;QC JsXUtHi z!90=d;]9kRB *ZOsG~3DE*e,i&5i E] e d"Jaϡ24jp2es DD:O^BYPE֨nd3,zKL)U387>/0kD91qq{ӯ%WHuO<{)"ˁ P_.q owZk n(iZNaNB͚ĈV=\ns6ΙUz- i3+EZI;zU)[d.S&nNi1A=6>8LSΐHtyl dr,DH<&jE|Z4@!S[gLdM$85藟dPQFlE+R3͔9q@oA9{-g|jNZCuoF3HBbO;p92Z ɃtVk}ăhQX"WeJa ^YYm⒆ u$2{\5lA ]e 6im%&c8Q Ȳh FqV7Cup3`89UI;)G-y#zP2uC 56t y"uC+׫2 rVEv?1!,`7lT4 ^Թ Ku;@yl1lO!RƑC$H#=%LxUji1b*C[*H5qű/%j*\$FJW=i ]4.p D)ڃ`Uy%NZL@̎v%#2gq-4!E3ӨW`cPӞi!&AXPXvAtp̈́@jz!|v&-Zo2'jH 4 QކgR䌅fFc/#'݆Rk^l<Zͣ̚,Mi|{|,&1 V C?I \oLB"gܳHr YTDNʋ- @SdTU"1CI.>sO5"дfBSt Ԡ7XkR/?NMjjz i$^^t@seTӏk%E4/aE bj4Ԋ|qHoY@VĖpCfX4JJєdV5)IHz@eİC8=5|xPOSd kpSW,Z⼊H8 ^>\p.\g/MjZμ/64Յ#7~C%?mS2NԼ"趚/JAwXb(xşpݶ=Um^'Y̻OٸhEmup1:'תxwƈ 6FP?te3Z`8xrg^(k-m!V.i95c{ [?yẞ>e>SIrtv/ q߃M[V?}_v::3ZPU}RVd}{*Zsŏ*gUy-U_QԦrd?Ճ&AOK}教g׉侟*&6[ߚ7#O,[m)9Ϋoys~VTx v2罝^?g7'ǂrIy!7ZlJt7Ǿ蓗U[F^wUY؀I+-9:{gk݁D*O"I$)ôK'/UsϜz%#~GGiulˡyoGp_xuӮ2WT'BlYS"QCq)+e_B@>G B{ӭ^%vYhĄ;Niؠ+//3!-׾~{|_eu#zcttfIB&wrE?u/x58.Ɂ͟5^6wWx.↛{={z=5ꤠ !+/47oH?E_rޕk3$n\r<;y}̜{-_̈́79vK7Ow=&^GV<~_oB57AL* .w %y_޲&Dɖgh1xCrEzkEշ\:wŚL")%jlMPޓ86ώlg_53fꟲ|cv;c'e2KD|˵Ű߱B) k+^[vR_\?,O;ʄ%Md]Е{ZG:GuFscYWq5JBO1R746$Xe&I~0Z@CRC "o缴tg$ϗtuzrYZNZs_ +ẁ=ZG9{|Q餏^5ԬX\+W7Y\s6mV AtuCۗ|+0It]fJv YUjRYx[#="6L:)!;҄ ʢ5kZr8^5Ԭ|w[-${en雁`+i]?d#G0*T_Ln (%yf8-m=0wgꡙ"ӟ_T:{^ſyǢ4T!FZϝq(G& 00ų{ԯyζ *6j@Kި(-ޜ} x2Txϟ2klTQ췢?܅o/yo8;}ӧ̺oߡ3y{Y|dƬ2XT*T^  8Kf`jS:sѬkyjL P7[߹[nxm_Sj Zef$4 kxuYu+I140y,N2>Y:yһ{$W0ݯLWɷY֑*#:@EȎR ,`n^hMwH,=Xf.?T:& XzVeoگ:㚻oyWkcX]3oD$s]qaoNw.[>Ǘ׾k>gun|+9-묆ӤKft+{o"q̼[f{{ROGq5S|Uا<yxP.}wҿ?ѭ썋oXG-Qk} *^<))zٸ #"}tAWhCG6+__ϟG3.$ߛYбߙ_[N`Sh⮓fczju~`/_PQ3Wdkmfvwm~t$lsLT}IIrs߸kĬ="@XKj<׮~uZWԵ[|].3F$s?7k.xKik> c[g}=1灸8]F$IAU Hm IDAT4/)'dAȍ2m{ɘ׾xvT{^Ȭ!p0}^89^8?UpET5nDPf' L"do&ide gOBAroZO}q¥ ؅,&6j^_&G HYΞih#]iVY^ɺf==Ƀ\C֒ `s7oxW'筛=C1Gf rRȞ$%Fw;4Bј==`-iEo:SG,~8sr|F(||/LdN4 A(Ty*bieHv#a#h:'UP'V9dGNYœ,{>~=0vv8trtRhIt6 eZj1.w߿=wo{@YFW~7Srawχ"FA D׊cq'Ug :m{.2֣pVWև.koJ@Վ}ِ(EbWnf?ա] ?q,Y4D8(UzsBo$WߚD2܄dC_3;˱Aw-]ސ:woY>eT|9xeT* LWG1jJ]p }sRQb~dH<>1DSH"PsFCA:W].z:CM7BJy?z)!krF?c  tDFҏm EJ|:54) n5ƚiBC0۴yO- "^t3+_(v]zsc`'W^*c@ÛVV]cp9~&UaĖb@ъM_V ɑ:O{鑡o[PnlǴΪ_d; ;2rY/ "(vqP6Gosdѷ-dل(߬=$֋\ѥ]PgV4-[6#G+^\7^Ŭzƀ [ׂ'7tfYg;cM%%d~AvqGo'V'TC뉀,=0+T-8אћt˳K051G?y:!7wւg UK^^[M`-7XszcpSnˢ`1`^O~Sd([mE{boRJObXcJ41%!΋L̄޹@Co}ڍW{/_>Z[vC.uC\}?r)MF}es\>P( ~|G~~?e _g\SѸV!;}ш|c@1WC پjg6g7k6ru>ʋ{8Pʡ#^S/|yq 3_kM;ٖ!2C\U%,޹@UC@rݛsWZy灠YW\ݓ?C{GhH)JK{!Ih^da,νr2P~ֽ#qw3/<9~_!DxǪ+>}ꃙ}p[7|Nn{_`)2 ;Ú9t|Y~fޮo&抍x3G>C"[;gs{MܪG^O֗UQO~~: _]f }|/w㳥^[}mD𛬶^H=E񓣻=>gu/os?[P(V۷[^}o۽~{ߑwddh~-7df;~7Nךȼ#Uzv~} {b^f/N~]˾N+͔bz~4)n34y̞'Ȝ?:}^"DDpe88۟e:}Y_?Og9}Y._'cWNJ(F5Ħ`Rv$PfXE>rɠZJYq IXy3sV[:zC9e=QpRyr *#~f4Z'up !d43.,QF|'XTq-m/_0j.LMtiD8S"P i[szg;hefv:Acl./02Zicc4[.ƭfsThy؈и]ftL F%$x)I#\ I4S}܋YMv Q22qnLW YR9Hc+*Sb:=m= 蘆F[࣒ ͊'njm&j;!dIT]{l9e*R[`55q= 4aD["‘QN̊WaKP2P̞D;t͏B9}vHD"WeC̀!ҔZR3Ad>ԜÈi,崩 `bs5鯂|ME@DMzE\"jH#!ߔAS!lɸp53t/P+gH=c&FKH Һ 0.wWq>Z.G4f 'sڜtr W5^b#*],b\J!hB&@D ">'%VjFT |5Ҧ yAs I!S9__ԭ~b+69McJCR1pPA swҹ&s&/fƒx,+6ygG3cvOXܽh$VՄ| 񇦎,t/JI"U~>gRo2gFwbHX2L5v|</m ^~`3[˂o[]SX[sr۲' X9ڜyµB5o\@*?05GjkW_[8YJ|WX[}ORcf3^ͮP_kɔB C PC$dDQ0`}=PCD{ 7 G Ƕx6V%F~8^9<"85"cOV1Yg,6W]oI њwƌ>_D:[s7 Ͷ$I画a[R;W(Z<j!HG~>ۀ#jeufFK}VObș3,yӛ|I*^!& sz}cyyYI˫Eczѕٻk{Gk.3)r3,0@6+ ^D2oDoStp5RJMo^^ In޼տl˰.3)/NOiS!6uɬ)7Mwe!d:izt`޼տ~uezW g Ϻaɱ8v,s`Xt?33bCYݏ5%%W-sׯoYZyeGT04wyW6?տ I ݄_X{ߩ"/iVDm!`.SIEU3D5絘X #^2;7L~ˈ3O[|7\cgƓՆ2c&&DZnj|^RSA@P+t>O5PzQl lH'%H_8+P"YMYSgjQ[1IMo$g_=DObAh(_]ϘiYH?)jEWiP>_y~!KSNul{7v֓B\eU& 6Aame?' {6?XYStϟ"Br?~?M'dM?/{U'?Jc`<ң~B_!Ħm׿my0rX]ͮۋK%"YwWݔ{dMOo5-}Yܼ6.O[wL>?Q][OU}m̺]rs3jLڦUYPϮTx =Yz1)Rx,?ɤHO 팪Am'.ԏLX&.('򓼘OYOaHh=ݹAL>+|,~Y|N@geEbb}ߟST_s!I#eآ %42:,K#ѝbj3j>Bۜ^qKqk8m ->*) 墇>cϽmylP 5K"Q]S2\G%̧5ޟpR~Q+1?ə$/^ZlD~,(Zѥ0@U~%:͚P 1hrw}ib~OXv|_}U(Wq9SyQ(->#E~ +e OZ%|ϙF1zNꣿ-=$J?o9]zz l𧌢Sf l0)s:$frP,$u%*"" +p׀,IA@EP 3DE@ 3290G{3ϯztUuù'=a)z%1UfƗ.[LSۼR"V<0wUBݾozo+o@iiiiiIAFʹ1"_QPHirnێB-.u#:=˜Lo|ph?;6InK^̭B'|OOa'9YYpB ]G+m D䲚~c͓,8t ^jċĴZO* /S/PXjfbzʼ7M>,R\ʿ0!i~[H`D+r G3QuTS=GLn߫U?XJQfo}s-A2 {m7/)(vݑbv`Zi5fԮy}F~xLci ?Ik~#2fVF&T)ە7i9Fc) tdORBm@8UK%D(V~6 ĴPrv|ÆK.Or+uլv WS[křeW|W;Eo.I뱂;)HK- 9tD ' |?@EY @Pc&}2Kڢn~~%.&ƭߒmIϞ! 6;#slb|-Ok!? Dike9sű3ʭNcZy_nr$eI'z|M1?E _og 0-1a2~cOޗ[ϑ:=ݻ_ێzUs/K]3ȢA|\frd41 %& >pŀum#M /kG5{Nz_}6߰iM7nZq柳.عeMX睝jbh Pٵ #ؓ$$ǶҨ&M_:'i~uPVQ5uLM_|uI62$-3UcSOwɰ/3CeMSIT4 #zVXo|l}l-yҷ욺!!B5sD-5䖠MC1+Z'TvwD_7y+n{ Q{}cSK=)ey`dy`(zֵ cP؟|ˇ=C?7,KI/'钦8OBx[~`#dI_I 3|ңn5efZ WIxFcdNS:r.L ' A I0l6o pB0\/`|bbSGsw6 ǛiU]ä~k %%i1[p(l- erLh%9o[9F;L>z)RD%?;d;7QbkĦuz1KN cet,ptgdXcsk4!E# Hb ضFcZ RLQKg{W3hyol8MBWD "Z%. uUor"^~|1h6+l}o[ =~".͓Vӕ1^@x'W"/醒^ .`]h ]DJ(Vɂa`' v.m wdټVMw-a5 ei U $~Ete;;1F1K/&,A7ۼJǽ ކ]- BU$KHYӍֿˢ8gAS2Y MdYKa]*Znx_D4zfFv*%#uU׆Lb"qy͵ꐭ`ؠRU1}ފz'eK7Eggw {.kU\^.z cj;qV\*޷vЫ׻os; Q 9JCv_,i*1u;Q')(/*ڻfO:'|P^QAyXUh`peE'몼(eY$:$NtfX9O8be]طaܓMgv˔f$)gYN1ϾPVnniA>*mku{rƚ/ٱvRވh$]gmweg'! ǕoY<^FfdڽLPv!HrqYiy )r4נ˃cu>l"_[پrXc>3i=<5iEn|xOnoFdOb/,/ʿϦԤ/yQl[ft$נː}wMff բ ?ɡغoܸn'xnm2P *Pz(Q(jw?:kyݰl?o}h~cMju_JyH H9R0ސ/U~q,fjV~ݥϻ ՝iBd]2gXE/?~oKfAf^&sKs.eHl遗;~B]IɴqK WoN:TEǾpBsj/,:FZ?PY^`,'t~~Z?a(GȄa'߳]VM]%*?I%!$vPxB|iWxB@/&ygVIkD)(E&sqs|,%\X f줛bpaK~NtS1f!G/*?Kڬ3r,@:IbDAk%J3g|2Af2BGzch]x *VsKx_Fpr|nEY$.Cʊ&)r O1έmˆ/^ÏG _I'ì@a)#+njG%w|nK/mZ?9͏/d~c͢Pyb@arI,?INxoM]!o$BH'*t58ڴM_*Qz. _‹2EQ~ϵ|M̀D CM9D^ ~<6 LvmW,}<- V#L% #0L6.&O^lui nEYۤfZzMoC6y򓓕2-'`t [ ۈm:Zkb>2kr 齧(T'jxoe)P!7|M$KI @ K'9g2N57XTyܢ?^r81^'ܦ `I(}y~?e\2-kM}b|#s{+|,I>:w4_wrݶ-jLT9Ж1I(<Ȏ%sǨ9W'[}[D=6yxt5 !s̅|$w~Ysl`~6UxOƃ}CAK2y<GBPK4($RxBҜ F}ٹP &R$g룶Kj?^8(RQN#Ds>dV5S—2BɩA(7wTCٮ ? ύ'GUDv%M3k73%1+bu}fFr?uݵUn+&'!ug5w۹ b([v+s}*S׻SjZ1PJʹI16I2:瀇%3v{+c}=_zL}ȥ z ($?9YS"T}k$k?zv?z։-$'^JAFnD94{'~E1Rn 쁞.3|EXiX$rC+Q6=\1 AQRwX-җ͵ χ:vi"?gvap;ٸwU}5ЋW(5׍OBΛ 흇f.oف`k,`Ǫ CO+xŘ%?nǢ/ggGi{xB5WE֏_W4?֎u#ʵ~Cʼn]XtM>{;CFĐkP;{Q> ?£&r+$}g,iԫOc(\}^2LP0KNk$X_eJJc~z_@TD4݂a>!},L;f[*KXcEՎ!Bq 񼶩'PM# #Zju|`bZ.y2*Di)>;mxR6q>"+91! b\kivUq:!pO^E'1T֍cC \(9ssƆ \BH7CCbє(;mCK$k_7~RxB~s#L DB ԾFP̗vsoHs+not"-d[7C:?_/II嫜;3rqV$n3rVx#q N(|J,!&tM!OK Il:qV[q'Ǵ|Y"mmD,JuQ&m $~$(R/sbQ75^;o5~mӀ:>f'>Mg*Ě2r}4̀"-K{fHyKWOI'{_.7{Kv[G]3kp;+ W+3txWȋI?B~X >Y},7{KN|K5擃s$Kl6 (KcA}7$=3kf=U'7I̲˃#֥E7$}, m2=j҄N__rӶi@{WcM癳1MSMVݶxAL<`ǴSK{fs@Px=ntC4Ҿ[^?Z L}3nڳ[ʎ}w?%HW[ |}Ͻg*8sC,f'^x--|_@y߿wκg*"9&t}͗GP_VS<[O_l??gM!GT32ZEn!E(AD7>}yyU&ѫ: ,Fq!s|^C <5 $~h!GUDO|KXmM:ecH(̋D׳!$f/HR#"3% K.}+"I/ ah(hEagKb@FAt@^ ;ˮ i{tj-BU<@[Lyg *RCe.X]".uxqFmZπX" =y$tyd^h$JQqB\6Z氀it.y2rl*0i$$p5&3Ǩ!UE+s)\LQJ'3ZBaE%[EjWxS]g/ߵhnaQť}fmkGU\Ҏ⢊⢊KE NT"j'K1l8QX~=SPX^\Z}5y_Sm=<<ۻQ o_J ˋ7+<0#% KIcGeI}Ikt|-̐}z.((+.u̡m}0?ȷx8?8-Rl{ N+2sf3lz??SVxdNGǟək9GnLwhd҂ k 1 tΕ~ի[Qbf:'AwE#}~gom݅{.bZ8-VHA{IQ[(?bw<32޿l֟@TU)iQ4{-^tԂʷ,_9# ˣK:wl_[m_5ѱSƙc܊-uxhٹ$kAdŢ.FL}E4}հFMgz][ (-\*sw,+T!c UlYJ;w&l@|/CoҨn; {MB6_^*\=w}ڿ)#1WovͯR&w>{{Բl[?3>;I;:Ó8[:, qaJҀO`#_!_޹hd~Dӕ3Ns,?IF=Č*ٌ'!_SdzBUX`̜k pِO}=X̤H`F[d&ϥ( v Z#ĺ%ͯLbRJܹ?J"7oᏴ6ºCV0Sю#Dž#}}xszĝ6` ,Xym^qϚ>?Wŵ,U{vj6R_&:RBLaiP!҄H h]%M?xus2PRAڗ+Tc @/&ϰ\ FʋH';s3Jsʐ7IJ$Kڪ$^"3?X C8< SA$`(%QU{Al8s3WvSyqIh/Iu(;7,?s _]-QH5Z}`26sƊS @ 27js.!!`%}CW1έ_ H )P>-6k &zw_}Eh^ ?IOT'h9b0>Ώf$B XnqydOcfV MZoR!HٳWVXI[;9'.2U@;N`L#>IDl'QT]D᷺_QO)*=觵s_xm_6PPdrKǦ\ԏJWY!o@j+|B6Cvaɹkkl6}Wr^& >喍]xo?2wjӼA Hqs0O2fu IDATE\TphO" N^Z 5_hڏ%)('%mlqNhv| . ckժ״}^x[g-:Yr Y8^o.oczo 1Z}NfĐn\9UZsUU,[jrk>ɜ> T&ҹ.$Qn_7,DHM}bB#s]pHO2kמ:V'CEWdU+}o>Ɂ5O<Tt`G.[kie\c7HVInykւq:vơ#/ &Ԡ8hhHKI>M b><6}cG |u[},2ىEON0wOYoҀvWoQ׽=9ia[N/IWvKpЩ5mѫ`[ِQ:屭 Ll~ 'tWQ(%s9n 6 ׉X?P DqDrNU-%㻼K_$y(7WEI8^H)iϗ`z|07OQf^ñ諱GNNҬo El呍jmh@QV?iYwD xvˠAm,p/r:9'引:=_zBOyjx~1)u~4V}4cg6Do]&}煼()m夁ROU<'Mla1vWXz^v\sF~/^_qJE5~04B&q_ g ;uJ7w/=i\=q)5kJAs?~p MO^6`>}K[0J6etEjZ TNzASU득H\քۡE*k1ӥkOOЌae֏_M.K r&:E@%[r\@ oͿ|̧KO[7B'Q*Ii Pv ႈ?ɩ2kWfRI><}>ϋr)XQh\3 k~,5ȑߏ.1K>{<-cDQ,7UVI-5hT~5~,J"]һ $^36 n_EfIQjxvTkM>wUo/*nم"TՁsqX5v WUiCFrASr._:i5vԮyw$ G8xVQB1+i&!By"5#O~)-x? \kE*4(b,#N`-N9, 15ʢ*Ikd)I7܈iK*.T7(1xev~&|A֢-yMSM?9Ǐ@ 2E\lwƆj@|'h {UNWL w|pn_= @nVxq*?ɖ vVyBDI[BY_ ViA}i=K'Q ͼ(}mQV}"jE¾@VCZﬧ5W\Ϊ5^(QfY"$ ׁ(U+sm۔a~l) ҉37lܴbeljju= bzED1ژpO\F?9uWA0ד]U>b={w$"334~ؖ^2Ivi$IͮNs؃*{wv/כw8׼Gޭ#NEƹlWhNNj] /e3Ż_~ꇦ]Xoi$_k$wvx`T~}Dn|' 5Sj!/,ʴ9wxݫf!ϋu.{JiyWSj|,7ZXϛLyg&9]=kͩ]=ys9lz>2MƽPu*6fVUNVFylhZ)}qMzTOӤu@e{@@$= F\:M? 5Б,?cY>@ #q8_l/ˇcڗǪ/n$% Ca(.spϴukc-S}ɦ_NU:~-Rj.Xo?} =ox0]G ±WJ߷Sr^X%"*9WiV#?G3 ~cɦz_m[B5qV԰׋}g-'Citئc%^T7 ?ʈ&nK% v og@JWӅg\SpLԎ@\"Opb0>1oRp$??IuKt1g]1u:jѬKDDTpeo>|]`|rLr1svubR">ػE|BrfWЅ$ԈMHIȉ8_*vC0XPb$V^Hh+01Mdfp -+*8t8;|Ǣ2 CUl#svڴĕ)8J:(b}rzmg]w JB[I.o`ʛ0k9!pKX d\.'xv,qv& @f-Yb(3OE-|&y7s!+Bo=&U?%?๏)nffa  mDLT$ِz;N4$zT4_!A)g@|Y*\zI$٩!V3ƣ;""QNsD9hFF%kJ /sdVaHUj8 C%AĈ>&]W\DB+Di<Ki+<%C8@Uz2lL9c ]vk4;Lr $䦶y$%b.٭T%R%GVX3-L% \BaT!(hM4P]mn!Ӎ$&I:OZ+W -re] #b 05P4hZo_p]×QuZwF\rG *rkadENJ>RsdPlV?>%Y6vz6֖Ce!{7es[3Ij, KdTx$yu=z:*2^)څ 0B~M|䃪|Pq4?E)ոx#递w(KtG%R˷4W'ichtsf J !4b^cOKda"DZS>ˊ<)G_hf8yFT]M?H&VkuWѠ[!kxճxaXI -be0wsR&h[mUhQWcŠn' N>1O$NRɦg~at/YP@ĢcTXetnEV 9u6Mo Gϩ%$q:%$볠6iE e: a'3ٸ<): gVmh+|mf(,KԆ%_)OԿ< ֹ3348{_̡mUG+(1tOƣ²חasE(Oe32hRh5uY]h1}Egg$&]T3#no;),*T-xo<'n?/ÄgʋƙZ?I~P'qK&~ /$!`[O=zοmqCFu>6Ev$ol%tak"|$ʊf˶`4k7-ghAr2]S$\HnrSS}Gm`!nϟN)N|bޮk#eDZ<#Z{nW\P; 0 棖G #3Uk2J3|D;6 +G\+,V6q};kMǧ^lۈc+[Io&9qr|] ZH>Q'Y3.}q?j&Z%5~Xk9.>_Iav)ۻVIa [ IlQ^(p| ˎr$"I%Gt($';¡}q3UPl d4ζ-62ΊG$M|vjr~?ݴ{ '$&1g}{ 26<uA㊩earGdS+立p1YF@fBc6*'^yq\ ~|ZNj@Ϸ>7/˹pTjO6Y2H"W=e'Yᒥ:?/.~${ /obsA5YJDh{i'MrB#s>x9$6E'YwJ I\^Xጉ퓘$@$f(5R"T|xYe-wL?z{.zYPfX(&ƕkxFx0per`"wbN 6cO6UZ-d)f%W$ vZ:݌ $5^ʈ7*t|Hb@lƙ >t Y ڪw0o.(txLs%VP- !!}&,L}Ì^ O|596=Բs?#˥]/Ke/mm3aG!/ORY>q@`='q"j| |:_\Rj|T0'LeK*Rq IFQg1dІJdf–O9 WUz$5ůHyF*9.@呥Ovׅ*XN/}ob+L!PTn26զ}=Z>tpQKO.#xvO@0C1^L4GJ k{T9/dnqj7~(T ($[پI\w3q'IP'YlEd+Nit: u@!JVbqʸ"b>v+Cgk)3u$|Zg턐"z%' 5z-l (ϞV7~KNow+kHxi?˭"˜'+7Uvg]>+Zl! Abp!#~dC'1xE2=x 1 K'}Jy~t'Dsݹ#- US& ѹ\$86{pf(-b@ZjՔ+5Yg~tƄ,`>iȼQL˱ L%;:lt}&0z\65n&O.` #5Ei u)8oӹJ$ע{y[ yҬcpʸAmJBA6SY ?03^ޱ)© 9Jf/al Q>ܤ ?I>%\0I>gI_-xO'Yr6>x<7Լgޭ#X箸eS]>U%T 6GS X@_cIuʘ>}w,'Y694~~ W'*ˉfːepO"ogƯ]7-zS5s=%O6W4x¥`-UXP(dFc@L\*ؿ>cOA<~O/_7vKX;|Vqe4 2a ^^I!\򅛎 *?;~R~q'"%lJOlϚ@=JC(N ꑤWh%p@;ؒ;~!? S 'R3t78.koǚ+qa\R;62EIvq' 3ذtN5XDYB0oha#rj1tq|&:k Kf2XM`RѲ{a p\Y@;ז4E _. !Ar0+G[k$$ nCbMNVy ~IDAT:y)\;0Q-vZD5s(sӠG|MgVCo)no1Vw*Jty#N% i4D28DmC ɥ DQhYA͵WV_a.;%Y FN`M3Uؓ-A""J -3ɒջ@RBOv)L$6;dxq`RDNSTiyQU\y"i<$N$yciʶFd=5ܖdH%%nF݄pTm<,xU?ّGtΛ|Q.]<+!"zdRلAPC3EEeْh&f]uIdt⡝I"3Q;(`|ֲTBQpnT {R^ v&pp2xh%ᚑ*sjqFRdtFf8[M4l$d DtiV` p"6+{<7 @cUSV8 a*u#bH/Yũŕġ\!V%47\%a TwQ 9hIN˲ tFx.r6K $d&$[>T(.hs_T"Dˁ)ɭգx./C +8>b5,!X#沍-e?$ٽjU#_"] -Nei۔!PMy?lUzh@( < Hvd }M1 OP5M0u#˴@iQtn7z2eC! ^mbpdl*<-qxMLѪҭrХ`OlM]ZrwI{XPQ5A~gPPt1U??/K5B^:-!亏gXpGVlA# OZ'MUvtɮNl'H>g A)q;dߒ\+Ȑ`?(BeaE~B+( ok+Pc&,g+I<gHٚ AdU^ΦZĴ+2S*rs<<ڼJ5(7[#51țKEy'9]}#@Sg}ߨfB#blJB'/kl  Y$("r6OE9J3@#j˜0/WP ZbGA] $™Q|o}vn31f>+&gQP()5>[3d:ɾ/F~66䋕ው&Dww:{O9 CW '7':{C@lp ٴ`81٤`|&~mM_۾V0SBo&*߱H@>+ ~߱,Tצw#E"ssmWZ]Y[,nٸo:YP=k1.mjz!T]6)H;vU^iOd'g6 Je-pTqC nrUJ5"vv X66UĈfsg̠9s`imk=YzmaѠsǭʽ۶+ej^8編2\ҼK>upO{<|aޮ6\x=S?ݹxx&ig~'E^FYU}FtXuJ`-Xb$נY|.%gtf4uO}`@W]@B0 y0a 쑎 2!5>`0^@y(+C Ve'IlBjdqMkB+j5)>B@31P4hc4XGMZn0b  sL 0tsp L/G-L4 ha1z:b@+lp C ~!@*ap_rIL$:Rc%#X^`w 06Jх~hb8m E Pt. 0$ Y@/C&@=E Z %`yp :2̃ @'/h9f@uTLުOo ozrs awlPm&m2p(" *@iқTC"UE7N ӈ^t :G @߰=l4рӀB 聋tg&k`h4X; aS?X dFvZv>}nT[F `NGÄ7vC%xA n<pT^bUzR{"<  bg!7~t,a`Ͽ(9tI]EAw=䆕pN18x` $@ b@ @p pl&5t53 do II9 eS 0=ao \(LJ*"tvpTWso  PZBpx |0 )LKsq.%P0k{0 @ `0 3ڐ zsrx6 i`QMd@@ @_B5E =n6:W3LRnf#w ?@4 t'p0P6*P @p6P5 t 4$ ,% {5U `Zxe@50a@pmQrc`6 57f5wnp9d*hF6x5y3q33v5j473 pJ0gK8v2s6!7fՉ+:Ej0p\D@d37e,!Tg)c1(d|(kq'sY9TsT5`C%V`X0 f h),Q(In=  +"I2orI`S uR0*?4|*YoA$vA- 27#!Xan)!1Z؜"DɃQ)ҙ5y\C+dy`ٞ'Ԋb92_G9%rٟR3 q2Y*Zr ڠq&$R0vf_RBES1A+).y7l"!>2)b`bҡѡ6)q|%}S&"%2'I|!P Ǵw3LARZ%Y٤-!_Zj$"1GjHz=n9-f1 `ڤ2 *$,mҧI ]z+rZ#r1yS[+t*N+3iڥ0oZoHpb"m#́m QPLo ⤂cQ%c%{jL- l Cr,!QJ!4,S`W>JLB?=Aͱ^AX.2nK)裝Bx– */#)|-qg'7Q!,,E H*\ȰJHŋ3jȱǏ#: ɓ(S\ɲ˗0cʜI͛8sɳϟ@ JF*]ʴӧPJJիXjʵׯ`ÊKٳhL˶۷pʝKݻx˷߿ L0ǵE:T P6EtXel7&XZJX=fl'D 0M 4J&H= &wU :!ㄜ1'׼DlY xak}u Bs@]AR2AAI4OO e i8IN$4N2/O"`X8c1'Q=(s)Aۈ(y qR?SmC B2Y,O3 6h@aJ3Rc@3@D3@3M 09@8P$cD $۠т=hbA$C1b @oD/lOa1P0& ADіtepq;@vS@B?֐}(@`W0}C@K*!AވCDxޮ?@ @;̠C7P=ٶ#3@jtjCOUV|c! *DF aI mXS@e`-dm@[=jp-tmx|߀.n'8\̀A!,.E H*\ȰCJHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ <~@,i c I~@(ࠌ(z8ac!H$ӼH&Zz3fS\}g'3a, 5Y௕*>] '[_bܿ׻}G0x؃ AŬ$@q,R8I|g$@QizB% e1:qfLIAjAUTB+qPe&@4D@0OAE& 9EtKHTD9'NhЎCPq' QW &W1@ ICx|矀uAj衈&袌6裐F*餔Vj饘f馜v駠*jF!,*& H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͈G߁@^8`a  %߅EN4%ϻ0/0]jc"}ds@W\4Dbv_hn@qhCӻ_޲@ ae.AApia1TAV(OA|Eoq,`Ab @OԘF'$@@t@Πo#?g6P%\@#$N+([HN!@`= ? 5g6qP-q누@&̅@&LG?~@QWZ0`Y1@EAsm @L+ &>W  ) u@hHW/-P.Q$8R!?"y?0CTr ˍb1rEBrCjDPfĂȵ "];Hdd k0  gQJ#@Ql(BQ*N _ qr蠍'd@?0`B=yZ8! "JR8d^H %Rn $0яX%ZM`8a:$´9`MD.\ኁ+^BG5|I0_яMĊ;+H`ah@PRCvH@X6! {Nd%?8Ԏ-vAGX4?=TM4@ ?YcD !P6=!p 78Q6()P # CVu-O?gDt!E>=dr@`{?4 /43?~U,$J*B}4AǴ04H5 eMՅiJ&6F+Vkfv+k覫+kHVԢhٻo_!,,E H*\ȰÇ#JHŋ3jȱǏ CIIN ˒,_a͙8ss͞@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ҃@ Ry!p8x@` ;@)h4 a @M?rhFpb@P=x|pD%}s/_3 \_tMZN4(=0!P?vO9p СC&?DaQD:$C1DHm@ p% 9q/ P jb= M^Wlp!,xD|oP$m6:(A0ъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ jb9] w !,, H*\ȰÇHŋ3jǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˖Ķ0BŷpU^h C`}~ P7P #2DxIID %8Hw g '  4Eb b7a32:H ^oj,:u22| 'lT5Yg?(?Wc{v~oe"B(.a,/̾)F*4?)A$$ A Aqӆ 0,?tA4@T[BW}ٳ,ͨJ1FT"T @A M1RODClG tAVW{t*EUlD@t^!,,E HHXp!Ç#&t(ŋ3jȱǏ CHaɇ'G\ɲ˗08a̛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳRS]˶۷pݪvݻx˷߿ LÈ+^̸ǐ#{pDB2K ?,@`AM2k f# p@M|43z# ص1@"=6;F])wq|uD,[F?f!7e% ?bBh@!#h7?I,QI<K? 8_\rM &q͌ρQ P=c2p3?3|ЃbwY)o"<"`? P447Fs3,[6d$K\P=$D=P?Ed=6ÁX] L8S@ 5)03lt[ f@1?2AJAVcsPy#P|p@A#@ V3Li^5q-"`1a 3`7Pb?!VɀyB B[T@4A7lkQ&!-$!mC( 0wfz2i@:︒&V3;>[aah6ХHD"AED4 v8biݖ P F# 0, >Z8e\2!,Cq=tEDDL@A4n LJ d9krvQ`,=c  &0<+!qH;pC"qQ1 Bvaũ0T \d!@*@!q @bC=QT@spc>P" $`AH0Eu74n ="R=R(:h *yF+C5`:<*Tt& GVṂ [xbY,39 H:u A8 I?Lؒ*'U/iC J9yxA8ࡀi ސ0 elP q2|61HqƗPGZZsCqF \Ȁ>VЇ"8*XR& 0ޘ$! 1S>aAȸe 8J9qJ b:Cb GB⠑OeAn Ղ>HC,aZ Z`Gj4C+B`60q6="8ġ*P:Z@0'";噂8wGzX0b' H\#w$4F>Z3G>Lx6=؄< >яxqN|-)bQ ԠG8Y0谷Y/#w #R`L4Bbt$A,cnVV ֩LBfd-IMc[HX2@L"]FnI&;PL*[Y2.{`L2hN6pL:x!,,D H*\ȰÇ#Jd ŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ Jђ*]ʴӧPJJիXjʵׯ`6 0سhӪ]˶۷pʝKݞ˷߿ LÈc@x y"l)<9O?w4.;L GP İ/ng#p?D"8L},a Ͻ\L_Rc 5hKSpT?DZ!(B+n ^i HTUHMKEAga É@4QI G\ BE 4A#F崠YSDe'OgONuB &A @k$$$tF\DQF9D@ @^irր@9XPZ EBE.X*0DATfB=T i 4P3|ZJW/!r";|2P#%S#)3qR:3E$4N&A@@d:A{P Qː( b? HA*Ӈ-:h,ST@d@0`1k(3P %VfQЂ+F$iB.O:PiP+BP C*!9 e6P+H[BCD644B4C;fPACAh@Qi2{jCZGT"N9&IOn*w砇.褗^њE.n/o'U!,.@ H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJիXjʵׯ`Ê;uٳhӪ]˶۷ZKݻUk7Ho| V W8]4>7C,i@U(qYl%7 7 lADi#ÅpA>1m@R[C$L*$B("CD#Wtؐ_-#!AR Yx@0NWDL ԹP2d qPA p/)t _.Cqlxc C?~뇯ed˷:$EV@ 8RC0k`j+o- \!O&mB0P^E3@@=O?' ?1b[V޴MXi?8x, xB%0Qn/aDW@XiP  BX.YY }@ x$aO'`. "@. @ 0pWТgQ1W!B+1k zD`{@>}9sN N'.?ʈO% ]f{ M@8 !P6!b @G>TANTDEd|!4îGp>T+f!M7@=UpjAp"+KBM*TkAPXf>HЀ@X4P8'}`Uaa &O7i-}:űO* Ffd *@T6d$}PLȨ T2b40#JHR `/$bf@q]@  H0;@B` y,CȗuA PPYH!2vYD,HCy@*ِNNDjPxc Ar +BW!4g%cO/ʉlTh!# BDh`#J"9QW$͌"D;h !"!3˘(MJWҖ0L2ҙ8ͩNwӞ@ PJԢHMRԦ:PTJժZXjO!,.E H*\ȰCJHŋ3jȱǏ Iɓ(S˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳ%]˶۷,ʝKݻxojܾ LX+^囵LWeu| ?n @pCCi|&+7Hv 8G W+_N(}:p aYGSKJ1 ЇVc$@k4B6`?cɁPQ29d@ C;xCBq'fPm\5 bP H8OFU- EE5t@ALG!dD T?;HP7hQQ ݡA,d@I4@ CEY|P" )cQB57R U c@ڔdCLCɴ]Bh`1fK"!Їu@Ĉ%@TF}N(,0,̄IJ8<@-DmH'L7PG-=ٜ`@!,GB6,AÇ#JHŋ3N$` @ 8S@  ȓ;4P`ўH*8 #-l?uP4WAĦxƥ * O4zJsjYb0e#< ≀!,Q˘)ZxDVGR@|<)<R&>s5[}4 !fNy#(c"Gg@gKB0FfWN~i1P!C0x?MfwN x({2<(  4}.'PD4$Aӄ hQ!О2,9҈|?EyЊ; M)>lxL8PC=쳉cF]BR/tLD`dCT%y` k 948p8;tlqMB Eg3yPL8BA,9*)$ yw\DDˆ` JܓhR2A=3X4 , =!8 L,c~[O4&Fe@IyїG~?iY=k3?:%O`A?C!9$S,aL5zG9aO8cKDP@@ ȃI@`/45PH3M6TF;<4H<Y@ #.PV]C\6C 0 SNhQAgPr>x05 ЄI? N!A;@> S; CW;CDE4`P3 X@;3ɂ}#Etc2M`z@$8.T'`qNL.ZdD4TA~A1A08Ք< (, `I 0@@!#@l zV54@Cd#FPeH|rp"іFe. bP y:6P~ X#@COY  @CYʲ7t#"ܣ6#5I@PQd. " IaCy`d VяO"ZP (4Q" AAp ǨG&HLs"E (<1(Z2.0!Hվx2ibd=~b @}(؂HBcȇ78 G-AO3T"aGAIYYdz,NBc1' @]35$yA e&%h#(C({JWҖ0DH8.sS!,.2 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵW KٳhӪ]˶۷pʝKݻx˷߿b6`+f*d $?|.Bi ! ?wRk$PO ˶q < uAg \᳓L9?>99{d )ԣP@"4@y3Q=6Q%T4BI$6Q! 5~bB@DdE4CE?tXGpƆ@3*YHaBL,tuGNFp $3&|Dt?#T"OD ' ֕:CF|A=TB O Atnj=rֈ4S\AA$@pE?FX4i0R44z%QoCB?[#hQ,A,A\GI@2F &17~IbiQz*I,uz1->ϖC`>TAduXM:+x 7_i Wlo!,   H*\ȰÇ#JHŋ `ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JhBF*]ʴӧPJJիXjʵ?d`PHO Ex] ƹ(-UM1Z<ɭ M#"i) J7+}p$L@1!,w|aݵyj"A'V!o@EI: }S F|TQuEC1uw~EP@?dqJD$DBTH v> $";vm Mhi PqĆ< yjhP!xAܷkMA} PG iKu4'E$着*U jhT T@!,)3 H*\Ȱa#Jh"ŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH HʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ @x` `x` $pD]0-en8!D BG:a&!ͺc3'BaĆ! r8a_ ӧaC|bCp ŀݠ\ *h?DC2+AO1̜f92'2!!@/$"At3 yFC&,آO?!:t lG?d6D1K4~p~BN/5# 7P@ _ Ay?FL@6U@@#Ɉ ˰CfӘN2A.!;CMuzW~]=\Q DQ:B|QC=pqP>b 0cCB% # ͸z2J@Ѝ& R10#PV _Qa Tc0 xW=GK!a.$T1Q|<'Ą|@H9 zcaB0# B%L[?8qG@A?p=@F!# $ āhzR'4^-SrFTgYw^!X&KOAf0R C@l1L@8@i9Tl?88pR H#+ikM$5 #ь/8E> h~#ޘG>(`$JaUK>\"B04{7 -Ca@*9HDQ 6W Y( v I51A(Aց!#vK2xR5586VaA2`"% of!]Ox;8B_PCd050% F?‡%C0@A Ą 9B`p#KkMATW>^ l8HeoP ؀$2 HeAy$U @H#ihB?D TJn`iЄ@P#>?axL 0ncFe>gӘ*D!Ax G,0 |x$u=81+'@@ȁ Q ŸT fJP >@A2#5A81S ވAQ &$Gg2 2/V4Q/0h&` ] @cc 2)( cl"((q ČD)g#"F +.Hx3Ь2H(y Ky9.疓F2Dsn {@$(j4?00>YLbAOa؄4qVAX5FϽ&2ҽc/w?vFxɏ>f^OA!,,& H*\ȰÇ#JHŋ `ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhbԨ۷p.=PܫZX!!4I<"|11 X0@3إQ iF P,ff䃈@aL#@d@AHQĂ /T1O-Ww(]m#3 -pEXa pO0@ <ODpA?00M/ ^w%~tA7OB,DGUCL?N Xu!L?bl `eiYOϘb@[Њ4B -##Z Is I!P,BA B䠇;#XP?x"(@\0?2p>B蠇@԰12:j$%. $ 7LB=cNUg'ɐÌ{)P?N9@ ]d2ˌF9B E[JI1/i3XAq`?iLF@X#oXTh' \I:1@_@3S,m$!h q2ma @.37h@]TN;ZQϯ6PD:L (2(S=1A$' X)GAtGl0=AWa7PVЍ PE dVT b!49l\`ﰁ+H"@`i!@z#o3$24Q"."$@A `f@{|qhf X)D y1G+w!!"#fH2P ],hjR[<5@_Rs8K!%%( \ @̱(,HI1`,!8&IX"b8Â!MCH(`D3;ā)@Lp@$.tAd gH;C 1?I@ahSo aAu$:;tTW+H"( Bp~8F>6!@C=` O^~E06$DDzq Mħ2?V $ T >F T?xc`5?,Ŵ+ BZwcSMjWZޱmjC+`?hیI@!,*E H*\ȰÇ#JHŋ`ȱǏ Cɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ NIbz&VH`@ [t0x))/r'h ୰.Pa B)_+/6z MSpCx`:}1N˫"6 1A6A ph&9An(rc&TCwP (b | 4?M _24Al\|fP>ricQbA(ME T]U?1*AhO tFA$D i QitЋX=85BD= DSDHH%&AEO X PthQbO)IP'݀0 -5InOuAOm\BC H*B$CbDO}ܪÅN2MWМ aAЏ =1@xv$ B,DSM cAz d^AAX A^[3P~ %0Њ:$CӄA6$@ﴱ@bw'j@5&xE!?,<^qnD)? T?)sT @@S?NDC1xSfj]OM6AdiuAZf`-dmv_l3_l to$wx&g߀.n'7G.WnQ!,,3 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝ[@t)ah7 aR28넔7&KnI bRph`1m"4F P8$/")縌n8_@;|0Y?DyXᆇ$*ql1W4I'qqw'4PXe/70N"T`Wk P$J}c5Yz%G39dATME:\!A3Hhe`NRDtD"DX?pC4@DB#"D?V=f 0gQR&W#1XҏC9B"rdJCԤѐQ֎p4B|矀*蠄j衈F%裐F*餔Vӛ>!,.E HÇ#J8!ŋ3jȱǏ CJ(ɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴS$JJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LL ,@ X#X 6 p"@;a}w?bT"]w86h 192b AY%^i0ҿCl/aɔ :3l94;VX04\p ACFFS?Bzpv ! I4B"y`P, AB4F ˸*豏@ !P2,w!<@8t?U P;kpE9|pS?M`+_"DP  Q ѴxA28p74E@M&?T< L?b7W !4H?Cz[CL&:D5B8+A46+t@2.2$4oź XA70v?>[DAddQ@אHyA@_c ;ޱ6 }14@@0Ģ  3ćP#eFHG1C2/|bNȃ?`me0؁, tz яD,tp4tcCM p4j$EA`V D E-A$^"9jŷ( |"> r q=\4|FPW( ȃAڡ@q9予1 {a بAYd9 )AU1#In!Xt Hc P0HG1x3 ?qT?rzT#+"L,QPoR@fAF48?dHVH T%6=IrӞT)=~JԢH 6Ԧ:PTJժZXͪVծz` XJֲhMZֶ !," 2 H*\ȰÇ#JHE.jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx=ࢁD-\ƾY^LBx.~EN4gPC1Mw/I(?V3[d!4J?r.9XBm ͖BJh B ,"@3^2rlӆ_6@yY:+ E   _xC $0c >2[C S[M(@+a YQ` ! 3CL$:# ]C"X-7(,XPddG!&1P YA`A;(C&q><0cD@@r jp %x (F І`#JY#C=x "pA8%#uЈ(Er3dMZ!]E;+AUH Ir"DȘA|Q#Cz.vYk$&x?ϫp @tMBІ:D'JъZͨF7юz HGJҒR4 !,., H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶-$ 3!nAu-?i 7d4 Ve.{8柳A6rkZ8#Hl ),BA̡7M6i!ap]V߈ 穣;'tLYG_ q@|@nw5A]D D ;!dDsATOpV`D-D?  ,@l(,[,H!,,E H`<Ç#6< ŋ3jȱG ?I$Ő)\ɲK(%8ǚ,c"iϝ? M#ϡH*(ѥPJJիXjʵׯ`1: KٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘̹ϠnV:h.h sֺYHyOW,ptFW1a J#ΦIlrK(`cQ*_?h~,!OY8FszTC=T Ahl( C/Ճ$)U`IsC Fх?P 10, $EG\ta BA FXt <9 HP&C2QrBC"d?aTC B AQf4CE2)IP>Y| >EB!>4H:DtAf 4Et@Dy~*PqkI%\ACV1q0fEECiDRHu8tgAb>]o Jl6Rzt@!@PpѴ*G~&?@e9k`oO~,$l(rE0,4lsU߬<@-DmH'sΞ1PGͳRDuXW JO:!,.F `A*\80!Ç#J!ŋ3jȱǏ>J(ɓ @T 1!Ƀ/QʜIĘi3H,Eѣ"]ROJJdTEjʵׯ`ÊKٳhӪmu۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCӨS{uOba^ ,=0PHB  $P RN t: ]X7qJ eǡ0c M@dJB$N;Z@bRƇ>0'=NM!@aRxE@  Y<3|a?P=@@`@ xClsA rw?=@@n2QM64h ?0 D3S P=|I?<.} by5wQ5'xP^} A$FM)(UE?h$@/ `0t ) ?C?W |DCBKY)(C0SF,E0D '0|!4@ʜC@0T#4q/Pq 8S00մ)M%0EP z7Q: ϗ#@tC.T%fAp@djw".lP: rsވ`(X@].!z+jKDtTF0I$#@l";l`8O1,\9,N"0?`ɕs"4 u"P9вPSbNL#r9C0䳏0Z ɄN*@|0?d3YT ŏQ!(P;8A2va%S fV 6\z @TkiC8G@F;\[Oj!?qM @a,C@" 'n@1oG1p#EHA@$ /6}w7D`t х(;̃!xHAP!YP42P!3/4uDa8CZV# \ a̠\DcQL8 8K2h`$&C}x?pHſlODx-]S, FD98&$]|2, !t""+8:@| 3dpȇ`sBFO |-" b5PDiED<``B 0`Qx0 p;T'0 r՘J,<2)?%, DvQ?jvE 1A`qILvX'B `@(?P 3sB9J8@ @Ǹ?x $. mIM-@B&H('}N8E dg?,DP9ȥSd.-2,cޑ=΍tKZͮvz xKMof|Keaoz{/L%C$c~`!,.F H`*\ȰÇ.<ŋ3jȱFAzIB <Ȑɗ0c 97k C]"ѣH T)ANJhթXjʵׯ`ÊKٳh!BM˶[WʝKݻx˷߿ L}"^̸ǐ#KL˘3k̹ϠCMӨS6S0!X C xP#$q2㏵;'8gN33\*$6HJB;VvA(xyJ)5םC4O1$OM?ڌ'0 d 4C25W&UBUA(Ay!&/5 XPv aq! %TwP0 ICFSS@FC/ldL!P9lA(DOA4nC\sHC %Cp$   AE?8Le|ąB($F> "S'yN6pSCU] Q|~hR9$BgAEBs?@XĒb@ erZSW !PINEmGD F ,C4 DB|T 8bѮ99{""Nh?7 %d*0iW53ҷi;~RP B 1B>у\SCS% _Fb$?#D00)W^}КlY-I ,_76ghl{]p-tmxB|M.f'7GsS.A fy!'sgh S@!,.E H*\ȰÇ#JHŋ3j(`Ǐ ɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`$)ٳhӪ]˶۷pʝKݻx˷߿ L8jd 74 @@?(4? #"x` .kbm3+Z#~ BpA1,'?l(ZDq% 2L?6:]eotL{f`ҍ'xO'6q&L9Y'h B͌fQQd yiȀM&p01,݈" LO t a0?M?Ul}S 4D5 wQ 4E0hN"E?H,`X7ʓHlR08"\?ܰQÒh ?L 1RP=8"P|Y>8PbA $ & YO08 Fq3! "a&@.pt* L+vV4C? +q'4D=9P(47 @5rOس 5`F?!=3 `P'N>& (ԈYCcƷBA?@8AY\ <Kaq=C.@A$mRPdHpڜ[S 3ga;HL?9cjuaM.0B83Cø$GG+#(" 03 <ؐfX@=Ѐ23">hB 5@āL "P!AD$Nh ~A!0C_1"-"ܰN>Q08Lc|k.sf?>#m( cA:!l!`p2pxBbbx 107@t."^b6"a& !HpC?ju {F/pCcH?f nH1<@Uh  xFGp\@ >0K BFXً!OH H^9FJAD6(G PF^".L 4Ѐ (=̃ Y! c ^9hH `vF! @5""KQ#XDe~pBcU@L!؅@^0oBID[H @C ?DD$@ĥ9 AypBxF?8520JpJ \y&i dP*0PD>44Gi+57I %π4IpVfv|Yׁj }(bإ`]`#KZHYkc z hGKZ MjWɲ%lgKUzrL,pX"%na!,*4 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]zF8bU($ hJ՗g9v(sDJT _AY 4aTG $ 0M  ؠ0.4U &p@tB /`[j'm[$RuX%h;4:)4< X {k!bShϡ^yY cM 0cEKt1=Oy?\4]AGtHV@ UA9d?v ӂ4)>q;>Ww P/)QPA$(DE T?%"BP> btG, x\&Ó( 2[ iSQ>O݌3ϔd"@F> =)P"*\DLCjDgH&q1?G+G4@9GУy*DnP!?: aDS7zҐ?@8Dk2-Dcl*HB?IQ.tlEdPA3,@]A϶Q *?_E[LHAzdf|п yӑ@5 #7 Y ɕpUEA21]!@݄DB'q5r`§`D?@D3@sCHpC (ѣH*]ʴӧPJJբWjݚ(ׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KLF^12X|(PBׁ\3i 3aQx%?}fS?y{? : Tѵ([v1cI#;"7/$Qi]0"Nh}l2;P6DOКaȀ? iAxipP@! @=7|?-SaqFiS3HD@|Ͼ HgCC! `0X`zp:kI{,P.Vޤآb!$2!ˈo eIDb Ej1\0A  .BFzj" s@Z!pL ["i鎓xPh$U+S?c$I@n\Q ALRB94 aT1c8H9@!V ^Fbjbā#&20Yc |XwԨb3TBP~`7AD@aD 2YPɩ̔ 4kdB=5 ;a#p?`Ja[zȃ,/`"Py zdy@B t FeH-C1PTJժZXͪVz` XJֲhMZֶp\J׺xͫ^׾MRdY !,.D H*\pa#pċ3jȱǏ CHqɓ(S\$˄._ʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ La2&ֈ ᅄQޘ '5$b>@QAA-M-8p˜lTAD OJx}s̿pqrK0r+acԜي] IGA4 qiL!{{EHZFA =EUd)ttA $?GtHMx@BHt@<6@y@C`- $CB ?0F!7M@u"&<O"IcAd? XnXT]-)Qdi P"ai& (&9 $aEBN't?7O-T ?QA{Qp 9Ww,,j@4dA\T%Cel B$MB/4IdBt8k3@x$\~,@5hBsJTiFBW%5y/ ?(4xB$%FrgDˆҩ i|k'TG,Wl0w ,$l(,0,4l8ZO!g;P@!,.F H*TX`Ç p ŋ3jȱG>IȆQFT)Ѥ˗0I͖Y~yϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKlTfӪ]˶۷pʝKݻx˷߿ LÈS&^̸ǐ#KL˘p <8ѰJa`/ 7TC07wZ5ahfWh@7;g7)P8 F,P 7 V"B?a, Ճ-Xm u) Y>^v zs!*h 2}ܱ ( B L#Px"vE?qӀ@"D<tCOX$VP q 2uHX` Q =a5E3ӄ@&҈mU)@СHQPQ@A%tCܧXNτD=Ӭ@:29B@$  MUBʥ{(+ $-@Ed~8tpH$pP wL5" Yj& M:\Ѕd[.;KFC؄0eG@򠐍9 FTW$|V Lj 0xQA0E7ĠpǁWtK @4X<~-B0dChA!| "*.RX@^` oDA3e4i ȓ 8'P.8o. )vyWxCPAH(.$qDAEl;10B@#B\A8jA6H4DP ]MB= `@4p|`AdvJ HB;$o;0`$ H" A)`Wp";A80{ yb?4ڛVx`H! :m`7F Ǐ__ ,AQ@m?@9%MYb6$=-LлMLmBbྂ* X"L5 tBЁ@=  cN1A}@-BtOp?$?Qv-F@@@gD@\Jx@ E:jaA`1 uGou;C@<)$t?xHB7$3Ќ 'P YA EDEX4G7P!,0? %!A9e&A"7s2@<!>DA(( M@DD\t5CdeCB3Q0*FQiqљń>'F |3<-2?1QBiCED2! (<툐hUBIuR D@fnq2P19F,EG 0P5Y'mC;!9P+ E*7x̐7RWDlsA@DESbD:6#*$ %K/OʜI%ʚ_)pÝ>ɳѣH*]ӧPs J4իXjʵׯ`ÊKYgӪ]*۷pʝKݻx˷߿ LÀ"^̸ǐ#KL˃c̹Ϡ?kMӨo uUB}4K3f p ?pJe+ ?ȡ TWɘQiJ+/@25_ \ 6N,.? }SP0a?[s !P8P݆Q )pI > = ! P8 =DN|X#P "Jڵ?@Qx ?*UG-!@ E@?,\E?OT ֚|3טBPd!rx `@̓)1 ` p?TO >\H G4%>4FAl;Ax? O pAP@k,?`j&0P:9Q-)QP<1PS'=Y90IP)bT,qSM?{,4O |9pzQ ?1N4- %2(P1A#(|wsaOq4`fA][A =MBCiC CR&L9%\14$Tp;>sO? *̏@ZCtB9'r2Cjj%H(*:䡏`BA<2g)Du "m=Q<CBq OD3(Ld`u(>)$1u+$EAdB8DLlA-m9PWE tdG\ B t EÑz.8=4mNFVN$P.N' f A$ @Njt bRb& 4\uhBbdN^ej41{Cyq3GiP @&BɐҔ0?FiA*t#GPDTz;)1145]*!aL a܁}|H#Q-TWiWyB+M 7G,Wl|w ,$l(,0,4l8<,D% !,.E H *\ȰÇ#JH3jȑ"F;J )"I'Treɗ0cʜIM.oɳ'Ĝ> JѣH*]ʴӧPJ tիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L+^̸ǐ#K*edHr*;  @*T/A>,ZxdFRH@AD::',6ZX#@ 9<"6c0pF?l#@Rt@ E!9 )E"+tAL?ٰs!&{$n(} |q 5\ P&yMB30AD7X4T=G@ =c0@b$( DS=QF!BB^G?CpRQuH=bp?"A!T*, -BRM1S kx@?D?$CĨwa51,@'E TE?j#L22V#F4 ph 3$3@ -9P-1gcB0FBT![Y3 ԗJqjOy s6}&|3H#y !d%C 7OA CFEB2tJA0MADD=xcqy'(}D!UCJda,Ї9=. WZ2=/> S֐pY+ @W>\ @OA=JA Aҳ V^>N0MS'AB";1C_PoAO@,?@+@|G' `\iԷQ9&}(b$j XFA 3;'tԨ(`#DR(Dt @U\FpS +DRHڤ`$m )Fb xJ#7^ FX` z+?^DXb B S\|ԟ Al rAt!6JBq!4`EA>9y$d [0@%~Q| aS¡@  ql F jy w s&!"+!a!cX;B$#u XCMwhE?H_Hc}D&49)!20*AR ,*!M"?b΁ 60Ws|2,$0  4Ȑ,,$eBL !\F FIAC? Ȯ |@;1o|C1xC,V@2Idb%&oB^]<-H [CI ={B3k fz f5 ZҊMjR^v?lgKͭnw pKMr:Нi rE$.ns!,.E H*\ȰÇ#JHŋ,BAbIɓ(SFd˗0II͛8'ɳϟ@ JѣH*]ʴӧPJJիKbʵׯ`ÊKٳhӪ]˶۷pʝKݣ˷߿ LÈ+^̸ǐ#K.lz@{yA *%ZQ;#Em51S" {L(x~'b)_f1+J 77:i!Uk,(mj]'ʺ7$T[TC p=oI]S?eFJ:ܔX$,Yc@_Q3Ă MSQ!{4ac"E[ IXY;3Rr @<B5X )P is>8u@9%36r/nCP C>I?"8Ap0= C_h@8ӜE^OBC A AD$A @cpABw תU r0A9Bd i0Q98WkK>dڹt`+k,Jl' 7G,Wlgw,!,)4 `*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴS(-Xr JQ 0DPFx#CZg&4d_R"]?w?`E/{&U?rHGB] 杨!\=xЫ{QBHUg3>kOnl p!s]" 1 èy:AQs  D `1?@rBd b? 0?Q'vlpA  Y P`LBWO8:#="&+ A  P1#hE[BqDZP7n@@! `+e=EODJ@C}s!Bs?8DC'x@ ?H#?:# ӏ,aBWD7p@c 6(:)ۙA 9Iq( 9RTS?q15H.rҸ <}Ѕ8lC`9?:$lw$pAwIċFRAPP;2<(@ӊ?(2q+ p'1{Tu2O?lRBG4@1 }-Rm?̃@ 3\3O.FA?\CEl} =$v8?C{&Pe>J"BDĞis5 1 pÄ,CpH!\&P%?|֤xqV@Q9Bj8! ]8>/ sE?g';@;`1723 pj0AFXK9.2[A62&"0%Q 6[|B(&E@@v+ N@?GPd(@yRa@XHC@1D|' Jc RMcd@b1 1 %qx?P4 8 ^L*'e{@~xH:!˃D "9bG", @q8Y6CH؄@&# tZ`~i+8dxD#T6h"|q@3 WSlH,5S%?zsL@tP0>,@`F?{))P)0_4E67e]L5D DQ$(ya*ވlp@;MQ&h   P1(T A`JA$A..Df@@`q*, ĔC6PcPF?iLO`vVlp;DD,@N t,AtC +B0TB$@ _tB< r0΁F8L1P=@!P3P?j]+TFB-DNA (C(B$Z CX?L'3P '$2#PZ3`?`sZc s@!uP$ c6np eK:AP`8*ep2>l /US4=FZ  @2g>axl0A>6D8d ^XHrC"* <7Uc|X@E/@ (,lC .b9JV #}b ;0qJģl@r8EHFZ)!A sTHx#%4PH>"2 |^.3=W`3Ȃٹe RdOjMj?պOdC` dar-Zj, &h /B$50ӳxIT Ԋ*]@ou2m:U$6RIn}pS}a(%L`r(/"T)ԋިDɿ*c&p G;" jF8NN6u.AL&@PC9""yikL1] Ҍ1HEim5i`֖x Km .G= _ԯ!W,_u EV& 'A!dWRf APmU1Q q ${rTbl ?^ tWFLC^E:4BGT,埉JϋD&SPJF)TViBA]\v`)dih&F馚!,)& H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴSJJիXjʵׯ`æ$ ٳhӪ]˶۷pf]9"`g ¿wUHM@_|5jTQ-12c/'-00GEz'G>җG (O?3؀hp?1Hr#p+V1{!M0}ʴX!h H@8?S?`%H3E]=^4ALD[tar%T@c?H { , |H"P!ILPxT&gz39A tJBt(A@Axs7qO-(@&RP+HN 6L?sFz)Z#UC?1c"\4,tL"\ЉQED7@$quPdC զv-AO Q`E0䳏/D|H`5L+|V!T\DRlA E\.ѽCPI0lt5@ 5Hl2ީ]Ȑ^d@j!B\K"IBj BaE ~ ?l`w(p;ؠ@`b\` {B h(AI1s` |hH*2$@"59 BB\G1(3Bp,İʁe{ScEHb ؓ% 9DܵJ{4uQdbjbz'zSYДꒀ!,,, H*\ȰÇ#JHŋ `ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶[}ÿ4 ?p`@:qdd݆˞Z؁"Ә[g HlgR X䓻 _}oӿqHL /I'\/kvh๺s_ j3wy@_=ѸF.l '2O @M" T?U"h0;߈8!? .XD>bs4L00Kz$?q ( C3l[y`Xq`bn?LCs<F3>،>+n8Ct@6d 3@,@t<p ]V@lTBdMDB^aD{`l X4чСe/Ht@y(@ ?P *\H+!)aiT!aN0)DdmCѐ$ppax:\aS?vuc 6@a0AݴB"Ҏ6*8G\; 0* l # b:"<8BH,`y@G qx `=#;k5tTpH  wBY@\2,89j8$j@\LA!H4‹  !@X &~x4tgC 0C4цa",*F ҄P3U9(BxV$ CAVB8H0` 5(bpTzՙ@J F\af!xA^!zԣ6k&cV+O PڮqlA>5&2K`I5e.@W /?aݐgZ~#ޗIzα|K yߓ!,*E H*\ȰaB !:H3jȱǏ CIɓ(S\ɲ˗0cʜI͛ɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L_0h;  -'6J&#! 63' CAbћ*c>&~heԿLG0F - 9; U.5BADF=!FPp@]O9 XmyP=!(Pw:Rkd#HBtP$" HS dKX1'u $ H 9NoŃ 8%sXDJAtJK0@ >CxB_??@ x@BA@ZB$FEB@ AӨ$e Q8 2¤CbQFYAt BRQ )5&͜m`P3-Q4 IfffG]ĥ@䥲)?_PH.ÌK 4*P/* Q[@Rv$B &d$`&ؓ@:0c *{HO|R?l38so P @ m5v>`(8cW@})&7S4p &1A޹Ek,bBa~4K첣rH+$XPj9SAM<0Ӈ'W}OM-%8Q3 n#P t**0B!|&@keCH:l6'@P0QhYTJAAJS~ɸ)z,@@f D@xP>I *dD83@a<G5Ӏ sUfD,T4^AIAYJAM\.i#R:9F+;byj=Cfe CI􂪦9^ha&c @k%! cЗL1Og5g @}C uVg$A$seӆ 8/<3r<δ5&8;\mP@uKPeCLHXTI?@* 3-{K>A \A2kMYfFg\AE@d :Lc UE(dG?b xHP>D@DpOhC x7%܃l A\O&bHB"X: 2ċ 7A y?`e&@ #hq* @"r+A5BPV=U$YCq48!U@ v A* &A D&%@&q9L@c0JPrĉ@ nF8LyBEMFrE=\YbXf8! 4@ }1D0z"8ARxcD9-(dl@IbDDwTDT HA}Ca@d=ev! ?3?P8t"THRb$kBN6fTUA-R l5C 5fڂǵDn\}2vͫ^׾ ,U$S:-a#KZͬf7z hGKҚ)=jkŵ" !,#E H( Ç#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿5@B h`DzoZO@`c8aT@]6@s`NL)-@'i ?/@#N aV*G@[ E@ \>gH,ߙ 1PxOC@O-`A-61O9# gP9V= QW.D#A )$4BMnFp!o P?6>굩+}[I-lEn>VkfvDlk覫+kmVKT@!,.B H*\ȰÇ#Jxŋ3>tǏ CIɓ(Shq˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKnUv˷߿ L \ H-A AÿZ"̠? V\睖 .#P0Y_+ l`K?w˜@wf ~K.IT%=F`4}`N>Ghf9a>P fu *2& LHsWĭD?qA44 (EDcP`d8' ?xԃ9֤!l@.`C>P|q&O Ueq7P7`?w? @@KB/ Ѕ?ߠQyO7TF Wx?=b݇P''6 YO12Cl ԫF$E@ xӆXП!BX@4tBq@h4iˌ&pV#t !#$P=%QP! ?VС၂/Ƞ< \q>IA_`RޔB#3p(4 @j[@{,JC JC ,SX ;iЀE1|Hя>1q5BpJHhCŒh@FXq@!…A$C&?4A$5JA#؂Ozଈ@21n_GBRXcΑ3?CUk"QHm?D]d(dAO?0'u(0-:2!wa23& P 4P?:P?Օה>&lc< BW dAJ< <؀@_"(~00Q1x |h*F,jP,$A,!-H2RH"R8 0/PՐM[MbE(iXQ8LX5d(p|M2 xA Ɓ+(A ъ$|(BBAA^# сA'_j6Q@3FDpdž!2ԗ `j! /iO (@>V5HuPc B, 'SDWp(FЊ~B0Ɓ3`B0qs}>zh3dP:9 @uUt:QB@V.D@h  K;?f P<0 X48i)$ <|,hj c $x> ؔ 5(iNX ky-M!%Z@@JyԺ6&&lBkgKͭnw pKM;:ЍtKZeӲr0;humanfriendly-4.18/docs/images/spinner-basic.gif0000664000175000017500000012137413055423433022230 0ustar peterpeter00000000000000GIF89a.F                    '(&'%6*" $ ) ' * &&! 355 2"(!'$8!+"7&9%2%%&%&(()*<5$98#+&6*(54+1(1745:667G8I.I6+K;+J>48E4OBIB+WF)HE4FE5XJ8\S:cQ;B "@'G-O9])+G5-H<=I78R-6AB z[.\ܖ_MW.Z K(] 縂#}mѨ ܹ٣k|3eγ5 媵^>hNwV|gng}_|8 %AWxhAQW~iGYmy!} V]}GuY]GXV @oUc?fa1i 'p1ip=XN"i%f8!x+㗣Uh3 ikVvnmG&FY晧? DU$(\pږg OBĕ`]Z9. 8姤NjlgZjdf9Ke*֏:[ k&ZPЪw\fV@r+k$pUg&kooc믽]̭+0gĦ1U`;qdԺ^r,7褣쎬r%Z[AH4Y3\^$ՖsLc-oCNXXŝ@BY0A 0@rYP@*U АI`wV ( Ip $g@H aP@ΐԳ.kDƢ Q< 3VLc 5!^8#" ^Qr3B@>F,\Jo(PtvJ[B[yg N@n F?ڐ !8\e[ H"gt5XlVJ }JР 5xF\, X@mapW]&+;͙h0 PF,XHxG "gX#`E5PA^&蕰IlBx"6 %<3-'7F ,p]"EMշDaqjV IڄF>C FVc$= & X@bX`H nCHVm` I ! ఏ# |rg4D+&&tJ*B  IuЇ`h|J#9E4X`Y@4 vD 17@8@@؅%(!? =& @8])S "2%qP3ڱ~c =̑cOhl*(ja@ EX@m"ΨG(psX xP!`7z0#0 dC 0|B(`CCC1au"װD% ((@pXe*,c؇2Xяf(8<щ9bPtTV7qO\Џ1!Vя- XU\tX΁cx@`@%QϠ x@(a )qa@ .  y0 QO'pE? \(@|p7$v79Bb x`e+[T_̃P0p, 1 wx HA1q,s; T@6JdC "@G*>6s4BVT cZ@FMp? ް@ p RG  6k#G#}G K6+P 0 p`M@<.5tUCE@_GPit7 0H^` Bpm Y3&ȁ!30 ʐ8br P ip(& L  @ %P&e0&p l#4\0 P 0& ` ݐ P chR' }O@zp ΰp L2D@0 z3 tdP{`U;L:\VS 0f "u &p; @  c{`Űxpl# Y 8}} k9 0 UME)jP)}V]+S2+VM.Bב),.P9&Z7egcMÑ&pNv%>!Z6"Q}gyT=jQ6%dMaA0 `B1DT vS6 1\,"'!pI2O%a60DdVC C,<ˆm֠ 0 Pщb#g ԩb1BQG&T9//z:/:P-I1f3jCTO/z*DGt)Mv 2F$Ǧz0KJ&iQz/3X.c*V,4s]z$E1*hwb?o 12I &;!ڡ)Lҧ2KdPil:1QI؉ʵj}{:ϊ*z)ᙣiK)Xz**k;kk+)DA9a3Q) Q%HQ!,.2 H*\ȰÇ#JHŋȱǏ CI(S\ɲ˗0cʜI"'kɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷_ LjAk"iPL Nna`*袥Ѩ-2HͺkBbxR ir -sk A$ģKj|سkνO/HϾ]!,. H?\ȰÇHŋ3jȱGB"Xɓ(S\ɲE"NK=}:(˥ϟ@r)%*]ʴiD+FrJիMEFD ,t ֳh] e,JG s3".Fli ^sM\B#(Kj9*ThJpKNM:(B뷯_Q :(諴dsK&L1ar?,ۥs# 0S`_ #*Ѣ=?rІIx %1Icn`P; =&ubpE/ eOTCs54?9A$CP,Tt 0Qe-1@Í< (iCHE>I6PF)TViXf\v`)~!,.E H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜIMnɳϟ@ JQ9*]ʴӧPJJaҪXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L ^|rb B.ry #i4=uA:XJX_9l1 'zuƿ^*l_[yq*NE^sѕJ&S]GZG&ſf%LGh@xD)a !adl ($HY,0(4h8<@)DiH&o!,,, H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝK]W_} 3y*9ְ+;1ZI<$%BdF]P5tKOp@,@3E|Č5QH@9$KV*ͱ$A$HqK&e)(Xr dȋ/ki "2 dpA ;fdIE0f[SAl-ö*8,NZȤ Dlr  GU|])#}U=; Iy gJ6PR4d9!,,6 H*\ȰCJHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴSJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ ^B`GBr@Ɖ3/l8? UV0:TY1Ay|9dl3O۶q6n &ؘRG'$FЄClgQo4.)ͱ(Bg0V?i(q=d&%qu}#Q]2 <G i VQ!J&jFÊ9AԦ"CP?,TBVDr4BJXo`y?@s8(C$? N0(]Q-"`iTe!0F@ep*_Jl4$B\M3er@;vEA y!Иq1Р (E&.Tj^"f*A dʬ뮼+k!,'C H*\ȰÄJHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ Le f?LDUh*kN8i焗i`1Hfv8  Q`0cb%;'_őIE4м 'pDQ4"C / jOHD -@.]-4@0-1W AH$D H@dQBIf4j aA 58*c(DiH&L6PF)TViXf\v`)dV@!,,1 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ J怢H*]ʴӧPJxtիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ i ^lb)#e>wF ,*ֈgPJYG7$py%db>Pf c4>;&2 jA C3%P>S 0x`@q5qxeJSU28 }f 0F8GP1RQqO4@fRaqOdDdvj0|DGHag,c Q?DcAr=$ЁbђQ P.- MCb@y@Hѓ # )&D3L@fF+$Q=8n!B@ܹ6g )J"e%jdB'lT@dB:+k&!,.< H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cq͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pHWݩּކz6H=*vo6LޑRd˖TNz9￿-Tw4`QhҗO 夅L O_m&h1\(&te$Gx^MoƁb冑X릈5mfIv۔,P!+08 > dO9R0aY. -h "Z wG E 1? !4@Q~31 J$!:3m 4BDl^3s7/- #RpL,p; Lt?jɑv)4FXdAADِa(bBpTD0ԇf`0egI&gơb$F*餔Vj饘f馜v駠*ꨤjꩨꪬ*무j뭸X]o@!,,* H\ȰÇ#.t ŋ3jȱǁS\ɲ˗0#"aȈف'ϟ@ 0BC*]ʴOU ( S>jʵ׋>-": J۷+O^eǃcY*^\+1pC:M`j(Sf'_` eۣ~ OxkuY^P]il ?fiz}&.ϿQ:gh& 6F(Vhfv ($h(,0(4h8<@) iHB!,.B H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻxW., ^8P@`Khp ~{b㙏1x8zȅ gMΈ2%s kK{3L)ڇ;oEDJg<cETYBH^A_02D?:dFABQ(4hIh!?:@)DiH&L6PF)TViXf\v|V@!,,1 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ J怢H*]ʴӧPJxtիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ i ^lb)#;#?3ϠnHJ ZuWCj/R<40I[ ufg FP>SJ:0x`3ᕍ^{9W@x2tC* AMCTt !2T@e@F;HfG'zDɔv<p?=Q{U!.KD3$*M C(5I1PjTt?'F5E0QmF4C-dPdofJ  ISӁF"d d@+iAiyB+`C@P4IDA,KЅ&c:!,.< H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cq͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pHWݩݻüz.H=* o r" p-q`APAddN(NJti̔ArrB&֙@ 8x4 gL^@|S O_m&h1\(&te$Gx^MoƁb冑X릈5mfIv۔,P!+08 > dO9R0aY. -h "Z wG E 1? !4@Q~31 J$!:3m 4BDl^3s7/- #RpL,p; Lt?jɑv)4FXdAADِa(bBpTD0ԇf`0e I!F*餔Vj饘f馜v駠*ꨤjꩨꪬ*무jU-Vכ믧!,,* H\ȰÇ#.t ŋ3jȱǁS\ɲ˗0#"aȈف'ϟ@ 0BC*]ʴOU ( S>jʵ׋>-": J۷+O^eǃcY*^\+1pC:M`j(Sf'_` eۣ~ OxkuY^P]il ?fiz}&.ϿQ:gh& 6F(Vhfv ($h(,0(4h8<@) iHB!,.B H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻxW., ^8P@`Khp ~{b㙏1x8zȅ gMΈ2%s kK{3L)ڇ;oEDJg<cETYBH^A_02D?:dFABQ(4hIh!?:@)DiH&L6PF)TViXf\v|V@!,,1 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ J怢H*]ʴӧPJxtիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ i ^lb)#;#?3ϠnHJ ZuWCj/R<40I[ ufg FP>SJ:0x`3ᕍ^{9W@x2tC* AMCTt !2T@e@F;HfG'zDɔv<p?=Q{U!.KD3$*M C(5I1PjTt?'F5E0QmF4C-dPdofJ  ISӁF"d d@+iAiyB+`C@P4IDA,KЅ&c:!,.< H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cq͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pHWݩݻüz.H=* o r" p-q`APAddN(NJti̔ArrB&֙@ 8x4 gL^@|S O_m&h1\(&te$Gx^MoƁb冑X릈5mfIv۔,P!+08 > dO9R0aY. -h "Z wG E 1? !4@Q~31 J$!:3m 4BDl^3s7/- #RpL,p; Lt?jɑv)4FXdAADِa(bBpTD0ԇf`0e I!F*餔Vj饘f馜v駠*ꨤjꩨꪬ*무jU-Vכ믧!,,* H\ȰÇ#.t ŋ3jȱǁS\ɲ˗0#"aȈف'ϟ@ 0BC*]ʴOU ( S>jʵ׋>-": J۷+O^eǃcY*^\+1pC:M`j(Sf'_` eۣ~ OxkuY^P]il ?fiz}&.ϿQ:gh& 6F(Vhfv ($h(,0(4h8<@) iHB!,.B H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻxW., ^8P@`Khp ~{b㙏1x8zȅ gMΈ2%s kK{3L)ڇ;oEDJg<cETYBH^A_02D?:dFABQ(4hIh!?:@)DiH&L6PF)TViXf\v|V@!,,1 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ J怢H*]ʴӧPJxtիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ i ^lb)#;#?3ϠnHJ ZuWCj/R<40I[ ufg FP>SJ:0x`3ᕍ^{9W@x2tC* AMCTt !2T@e@F;HfG'zDɔv<p?=Q{U!.KD3$*M C(5I1PjTt?'F5E0QmF4C-dPdofJ  ISӁF"d d@+iAiyB+`C@P4IDA,KЅ&c:!,.< H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cq͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pHWݩݻüz.H=* o r" p-q`APAddN(NJti̔ArrB&֙@ 8x4 gL^@|S O_m&h1\(&te$Gx^MoƁb冑X릈5mfIv۔,P!+08 > dO9R0aY. -h "Z wG E 1? !4@Q~31 J$!:3m 4BDl^3s7/- #RpL,p; Lt?jɑv)4FXdAADِa(bBpTD0ԇf`0e I!F*餔Vj饘f馜v駠*ꨤjꩨꪬ*무jU-Vכ믧!,,* H\ȰÇ#.t ŋ3jȱǁS\ɲ˗0#"aȈف'ϟ@ 0BC*]ʴOU ( S>jʵ׋>-": J۷+O^eǃcY*^\+1pC:M`j(Sf'_` eۣ~ OxkuY^P]il ?fiz}&.ϿQ:gh& 6F(Vhfv ($h(,0(4h8<@) iHB!,.B H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻxW., ^8P@`Khp ~{b㙏1x8zȅ gMΈ2%s kK{3L)ڇ;oEDJg<cETYBH^A_02D?:dFABQ(4hIh!?:@)DiH&L6PF)TViXf\v|V@!,,1 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ J怢H*]ʴӧPJxtիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ i ^lb)#;#?3ϠnHJ ZuWCj/R<40I[ ufg FP>SJ:0x`3ᕍ^{9W@x2tC* AMCTt !2T@e@F;HfG'zDɔv<p?=Q{U!.KD3$*M C(5I1PjTt?'F5E0QmF4C-dPdofJ  ISӁF"d d@+iAiyB+`C@P4IDA,KЅ&c:!,.< H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cq͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pHWݩݻüz.H=* o r" p-q`APAddN(NJti̔ArrB&֙@ 8x4 gL^@|SKmF9̸cLn{Lೃ^Xk"?zT %lh y4@c .|\.U! .ix}B< X%IR _ACu d?X0B}XčCpL^&v"e0b֤4h86b@)DiH&L6PF)TViXflh!,,A H*\ȰÇ#JHŋ `ȱǏ CIɓ(S\ɲ˗0c ͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿j< È?'ǐW-eR b&ޣ,I_+袠PUv95sb3W.N ǷP`I`U9<eZ8b$r*Hsw"ʿZA,(ZD/~}RS&'Pomq`QC4 E8ƅ$hbD H*\ȰÇ#JHŋȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷p}Lu0,uMYз0QS#B 2ܷB*<3>(do# 1 Ad)&Py!3 t@`3>hD&T /$_u h?f馜vio~ 䒢jꩨꪬ*무j뭸뮼:K!,,F H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]4)P  իXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿5A!`\ç-kIx~hQ= Y!#Bž]U `IN!׈OGQw<5Bhu rY/3 N@El?X&D8R Q $ȊdGWLFJ(0F)T@BSґ&وf%e1@SaȉЊJddGP'E22!pE<DH43ci Adq~Sh]y &6VTgsGyv+k覫+k;!,.@ H*\ȰÇ#JHŋ ȱǏ CIɓ(S\ɲ˗0cq͛8sϟ@ JѣH*]ʴӧPJJիXjʵ+Ś^ÊKٳhӪ]˶۷pʝKݻ%yd!lS =OJtc@? 2XFm&pZ836, 5ZՓPriAU3)-&a/.n7%t,X໅^p=zC0?c-99அ*餔Vj饘fxE駠*ꨤjꩨꪬ*무j뭸뮼r- [!,.@ H*\ȰÇ#JHŋȱǏ CI(S\ɲ˗0cʜIɚ8s&ϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻxꥊaߗѬտ#^l\ |TcZ@]TE0ANyLPD ;_-0kaf=S M `,6)rZDϥ7!r!h+"hQ3@aeyJB"f-9Om$Jm`GYԁD\ 7 & 4]QbHuΈ(zDu)(4HU%ڨ<@)$J iH&L6PF)TViXf\v`Qb^!,.F `*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳI> sѣHӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ Lj^ H@ȘQ0AL?5F3Ϡx $ #P@! \˲NpĊVCn?:~bAh i,шL[k33E8MdŏDBڨGk 2pra Z+ ?O}4CHtHV)CPTFU\D/x LAX4.ĄEA DC:y?004DiTIBH9eb P̓5) 1=1`nr&tk*ÌBuGBhtEΉjnP&N^d)di檫E+k&,f.F+Vkfvކ+k榫!,.F 8`*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LaXС>a.k66D9c[i4lpFAGF G~* ^?+P mPBe;=p`GwD :Ph"$(TYa: }K|$@7r-sT(!q۱2 %$ JIrF T@x0D/1x8H\z:F,=)D6@82L6PF)TViXf\v`)Gihl@!,.) H*\ȰÇ#JHŋ`ȱǏ C)ɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝK]{/R xM<,װ7;y`%w70e-Pz'G41蟴Ls!0i89(>hw2pcp(,Q)M8B[/(O_ E@b"5'PhSD| )bR )`Ky u@JMT^B2SXa Bld@5>dBJ_D P p-x+39iQ9 @Oy8P`#*d!F\PӐWtA5G c|&DFA<F(G!,,F H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]4)P  իXjʵׯ`ÊKٳhӪ]6ӶpʝKݻx˷߿ kA\ u ac OH=DDmAK(T8^,㈐c[],iAYMuS[)vPB+%q3@<q^fكuпfwD #$@W2֑0S?㑄DX(@@B9$F8RP0ا1 h]aK@sPQ".|7:!lHIVT4 Fq,DD&-VH>B$Gi]1j)8P0C MJGw#FUĞBYeSaI@JdTGP'E22!ra 8 $G^1A čCp*SOU`i[nӠk&6{P ZVkfv+k覫+!,.@ H*\ȰÇ#JHŋ ȱǏ CIɓ(S\ɲ˗0cʜI͛8%  ϟ@ JѣH*]ʴӧPJJիXjʵ+ԍ^ÊKٳhӪ]˶۷pʝKݻ&{d!S =@OJtc@? 2XFm&lZ536, uZՓPriAU3)/&a/.n7%t,X໅^p=zC0?c-99அsn*aQB[YFkn`.@Ri 6#R4 m^ K. 졘OVXhbwXqf9Nq͔̌Ԋ,$"&%x.)  A[?w ++T1gl$Ruu 8״CB MB9@lz(rD-)0>1h8<>)DiH&L6PF)TViXf\vYW@!,.F `*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳI> sѣHӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ Lj^ H@ȘQ0AL?5F3Ϡx $ #P@! \˲NpĊVCn?:~bAh i,шL[k33E8MdŏDBڨGk 2pra Z+ ?O}4CHtHV)HlTEL7q`)܀%ISBLX?@Aw1A@A6p@$dD 7wPtYC dC_$Bx dOL9[ ]Ga.k66D9c[i4lpFAGF G~* ^?+P mPBe;=p`GwD :Ph"$(TYa: }K|$@7r hw2pcp(,Q)M8B[/(O_ E@b"5'PhSD| )bR )`Ky u@JMT^B2SXa Bld@5>dBJ_D P p-x+39iQ9 @Oy8P`#*d!F\PӐWtA5G c|&DFA<F(G!,,F H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]4)P  իXjʵׯ`ÊKٳhӪ]6ӶpʝKݻx˷߿ kA\ u ac OH=DDmAK(T8^,㈐c[],iAYMuS[)vPB+%q3@<q^fكuпfwD #$@W2֑0S?㑄DX(@@B9$F8RP0ا1 h]aK@sPQ".|7:!lHIVT4 Fq,DD&-VH>B$Gi]1j)8P0C MJGw#FUĞBYeSaI@JdTGP'E22!ra 8 $G^1A čCp*SOU`i[nӠk&6{P ZVkfv+k覫+!,.@ H*\ȰÇ#JHŋ ȱǏ CIɓ(S\ɲ˗0cʜI͛8%  ϟ@ JѣH*]ʴӧPJJիXjʵ+ԍ^ÊKٳhӪ]˶۷pʝKݻ&{d!S =@OJtc@? 2XFm&lZ536, uZՓPriAU3)/&a/.n7%t,X໅^p=zC0?c-99அsn*aQB[YFkn`.@Ri 6#R4 m^ K. 졘OVXhbwXqf9Nq͔̌Ԋ,$"&%x.)  A[?w ++T1gl$Ruu 8״CB MB9@lz(rD-)0>1h8<>)DiH&L6PF)TViXf\vYW@!,.F `*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳI> sѣHӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ Lj^ H@ȘQ0AL?5F3Ϡx $ #P@! \˲NpĊVCn?:~bAh i,шL[k33E8MdŏDBڨGk 2pra Z+ ?O}4CHtHV)HlTEL7q`)܀%ISBLX?@Aw1A@A6p@$dD 7wPtYC dC_$Bx dOL9[ ]Ga.k66D9c[i4lpFAGF G~* ^?+P mPBe;=p`GwD :Ph"$(TYa: }K|$@7r hw2pcp(,Q)M8B[/(O_ E@b"5'PhSD| )bR )`Ky u@JMT^B2SXa Bld@5>dBJ_D P p-x+39iQ9 @Oy8P`#*d!F\PӐWtA5G c|&DFA<F(G!,,F H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]4)P  իXjʵׯ`ÊKٳhӪ]6ӶpʝKݻx˷߿ kA\ u ac OH=DDmAK(T8^,㈐c[],iAYMuS[)vPB+%q3@<q^fكuпfwD #$@W2֑0S?㑄DX(@@B9$F8RP0ا1 h]aK@sPQ".|7:!lHIVT4 Fq,DD&-VH>B$Gi]1j)8P0C MJGw#FUĞBYeSaI@JdTGP'E22!ra 8 $G^1A čCp*SOU`i[nӠk&6{P ZVkfv+k覫+!,.@ H*\ȰÇ#JHŋ ȱǏ CIɓ(S\ɲ˗0cʜI͛8%  ϟ@ JѣH*]ʴӧPJJիXjʵ+ԍ^ÊKٳhӪ]˶۷pʝKݻ&{d!S =@OJtc@? 2XFm&lZ536, uZՓPriAU3)/&a/.n7%t,X໅^p=zC0?c-99அsn*aQB[YFkn`.@Ri 6#R4 m^ K. 졘OVXhbwXqf9Nq͔̌Ԋ,$"&%x.)  A[?w ++T1gl$Ruu 8״CB MB9@lz(rD-)0>1h8<>)DiH&L6PF)TViXf\vYW@!,.F `*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳI> sѣHӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ Lj^ H@ȘQ0AL?5F3Ϡx $ #P@! \˲NpĊVCn?:~bAh i,шL[k33E8MdŏDBڨGk 2pra Z+ ?O}4CHtHV)HlTEL7q`)܀%ISBLX?@Aw1A@A6p@$dD 7wPtYC dC_$Bx dOL9[ ]Ga.k66D9c[i4lpFAGF G~* ^?+P mPBe;=p`GwD :Ph"$(TYa: }K|$@7r hw2pcp(,Q)M8B[/(O_ E@b"5'PhSD| )bR )`Ky u@JMT^B2SXa Bld@5>dBJ_D P p-x+39iQ9 @Oy8P`#*d!F\PӐWtA5G c|&DFA<F(G!,,F H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]4)P  իXjʵׯ`ÊKٳhӪ]6ӶpʝKݻx˷߿ kA\ u ac OH=DDmAK(T8^,㈐c[],iAYMuS[)vPB+%q3@<q^fكuпfwD #$@W2֑0S?㑄DX(@@B9$F8RP0ا1 h]aK@sPQ".|7:!lHIVT4 Fq,DD&-VH>B$Gi]1j)8P0C MJGw#FUĞBYeSaI@JdTGP'E22!ra 8 $G^1A čCp*SOU`i[nӠk&6{P ZVkfv+k覫+!, .;H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`Ê/SdNTahu:!oNY!;"D%ᄅ4zYp~(x`kvXa#aPF9|@k޻aQC3& 6Z'=`pr9c 7Eu CbAQKB_m 0t| gR!0JUG BQD?H FL4S_=C0O_x4А#̓Yr$X^C_*o O{sMJ|AnIE:5IO':aM|b*chlF@p)TIx|矀*A j衈&袌6裐F*餔Vj饘f馜vb!,.E H*\ȰÇ#JHŋ3jȱǏ CIIN\ɲ˗0cʜI͛8sɳA> sѣH*]ʴӧPJJիXjʵׯ`ÊKٳhn]۷pʝKݻx˷߿ L\;`⩏O51DAFt4˗X`mBa)FF Liƨ7Eqsb!nV r*x˒0@}esIxTFj`$CAWu 9 ƞu-a-4q Ce(HpU@^(LAtDEňR 0D_dE͈`TH NG,TEE bdWR 8CgeEyY0@%DDoTew O;7Q$ D8Q)`gD,i*e CiQM4K<DVtU ijE3z5+&d6F+Vk!v+k覫Im@!,.C H*\ȰÇ#B ŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8ɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊM`ٳhӪ]˶۷pʝKݻx˷߿~1 Xʲk@;%@`O q ;\s;jcd\J[:q$hhwZqyi VCe(mં!J'pį5AtOAEgb ' `DZB\D-6VІaM! O G 9# IT^H7ha dDA54Gtd-ʄj1?eJD["$IB dMqPn}C?NFQuh Rѐ0ةR(P9j0s馜v駠*ꨤjP:马j, ꫴj뭸뮼+k[Y6,0!,.5 H*\ȰÇ#J8ŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ 8`ѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]+(۷pʝKݻx˷߿ Ln +J`1^:I8LgF^m̱pAFuC`&Y3TA1 IQ~z@#e{8'㰈Lv,^7#(?`+'~@&jA.pWdBB&dJJDD@"" :Q/Px I#G̐d$Hr*Y3 ɂpAs@iS!сj9O McP<ea-c&NDtBS=r͞r@S>|Ч09: $ Q D3 NI2Z,4%I74CXpDHjTSUp}kJ6lVk @@!, H 5 \ȰÇ#Jt EhXCIɃ5j$oLʜ9S͐(3jD(o r Hb8)m4)ZFE><S *2#Ykkh<P STwCf,)b°+x La#/IR3[,0 BH삔΁ZI|!fiRxd-w䋅qH$dt<hCb"``~PBq`KhWDc|%y\@| &G,[A ~#h+Dz 2Ky%Xh2?%\ϓAUU^R;>4I^E9Y4JmW ?2,[&c8K/k/A 5?_|=Esk< Pe-<KD/HD{2AIVoH7K[%HY  gکe0Rg Bh<a%r dC,ĨE7P}$N5$9 S*KաzyfJE'3;humanfriendly-4.18/docs/images/spinner-with-timer.gif0000664000175000017500000016062413055423433023241 0ustar peterpeter00000000000000GIF89a.F                   %%$ !(&'&6'92-% & - ''&3 477#%#9#7!)"&$:&3 %(%$*'&'2+(5)%:6&)*5)&6*2=6695473,5D6I=5K:*6F2+K0bD>HE7*Tȑ@HH$Ă'Gj)͑q6 9sEAbrF:E<%ɌL"D0၁ ZФɈ_M SWaa ًm6eKݻA Z2K VdQ\R˸qFDTUm\ǞA+3赉FŦ=MҠas]G~;4p٤ǖvϣ۽ }rț_O]ո z_wiײf?+9{:Vq闚]2t1X~9qYكiEW`cy)S4hciWWz2rգ{?nUt i!:da)^?nƖGf%cbw$cplJIcRjIudq&czjPe֛opY#rEPs)zb@-騊N.5EOi 묎J뭸JNx+w=&7gE2+.{@F[v k}kh{5$;[ ,,T+B/'$j#SuvpgyQjv5ױlwxqxrYbs1\6Wg BAWZ/A q @pmV`R\u X^&5p^$05C#@db 5ln@~ @om&AxEA1ԣ?Ɛ/\F}t+4z:أ!z[&iPP& UXhu^D@*) 6r@ _6ۊmum ȈF`Ob`ABZ p;@O|Wtm JLIHꁆ@@ X r N=0Bah@J4@ yb`m<ChmXI-`EMT4YPD !p 8@PV>j`Y><0,/|*:pdP!@ 8 [ f0 |pI >++A_@F;:0hhX` fPHP-?-\"%{@a~#E0Іi# |Z(D0SA?efHq"`((s_@7Jj#@E7 fB>l0 $"ѐ>XU(7 EQ 6>Qc# LXb߈BLCXT^1~@PDc,@S*[#!z @dD)%X `P*p9z( Y@/?0l"+iЪ`|+X?R1q=@QXuX)hPI k+? 0@ZpFQ).lgq8A@ Gt _Zpq< 1 y<`j?ɁKZ rBl~A<~ xQ ߐ Q^(GV `h[P `E2YPP z @±&EWNA0d,vL#|Z"IĐ?H02u N +NhzrЇЂ*x PЅ  :*n<(Cۂھ(4=y; (HG4 )|a݈[y2Mc h1 EM4M{<!-@$K #Wx(9d xC!(!CpCzqsF8C*У| I`+p@9t1|.7zxm?X^9АAĀnV y$.hÖ(Gd_2"J b՝7{11U]Y0UO 0!@yr@-#`>jUB x ʳU 1X(7#ů8HȡjP<c+M  3mAMU mhUWhǼ 0&X | WΓE2g\PJ@հ^Jŀ&p) uE;\c ++ 4 gkD' ]i0 C2BGCcmSW 00bH u` 0`|0  ' ;)5" f5 D\ U p ǰ 0 ``p MPfT$ @EpO@wYP PiF2$+qq1  Pw7t[39~x@gp \Pqs$d0_8+ЇG%$Pa|p: 0\I,`nP -05@ `S G(` + 8w̥5 0?0yF\;",4iE4?DCGJC2C6  vIT>E4nC6PC 7dS0p+Qe4 A\SP d9Dis7 I;i/c3MU'),)8T8j35 Tr;U}DR>)?9  aP G+aqB xe@ h( ӛC)j3X3](=@9tR3CY+q`@pWI59##~5XMWiqaR2YHs%z#֠!!20aGI3_uP #'Iǡ%M!.ʞ1f!1~-0; Ab1-;%N*."+&H*R*'P;& ic5jHRB)=s2\kCJ Z u.Ѳ&2kOZ}d2jQ\#zy ,I"vLʠ[82VVX&e!s'qf)Ғ "ڧXx+=3R!y"EKMi\ !4zHڨڤtWQ8>E~fÊʠ~[cŸӚ'Er$VHC$$K/ŪT8V3c8P"mR9=ɲd1*;Zor"qIN,%M_9'hh AˡjR4';'+"1da)A |q"e,!,.F H*\ȰÇ#JxŁ/jȱǏ =f82ɓ(S\RÒ[ʜI͛8sɳϟ@ JѣH* sӧPJJUjӪXjʵׯ`ÊKٳhӪ]e۷pʝKݻx˷߸W xÈo,ǐ#K&Zd.kyg>&`Ѩ%h`R{fM,'eOR 9ddyfJV@}0ek"%c`,`MW'YAq Wt3Il9)4`1>8p@ ne&=Ijo9 ynaݚ1F1-xXE cJ̱ 80wlp&PԃCQtKLE@DWPPAaw` @`h= ' In tDP.(UItuaA>sRfḙE9,l/gmhd隋&2]ޕwNW<)Kohh*)}q|J6Eb0tlS6Tn+;m9phe<:#by% ?qMQJVfO 2N4aVhfvi|($h(,0(4h8@!,*ES *\ȰÇ#JHŋ3jȱǏ t ɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷;˕$ݻPT/߿ L(x1 K@,1Ř07 m/|> #3`@;.)2; ,"fMXh҂eh?&eN)u]vkekxK= 򩎔h;ިA)]A96TP9MwYpsyP1}@JIA_UNX A) EMP&<D>AHdGҥT !tDB ;MA FD4dEd%;Q!pZS.TK3JE$RphZ?P?p}D!]@4Ep?=$ITOA&P'&tD`Glnf B +LA6F+Vkfmy[&妫+kklJԯ{ !,-E *\ȰÇ#JHŋ3jȱǏt ɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ I+=H1όmn i[K\0Dq4`z q@t 8 eB(۠{,-l1qL(W1ttbM7WE/ D(Ga4@}cPb15# `K4A4@ P$C83BmU O), } P$&bXD0bN Њ<$I%6dF8A\ATNI3ap?Bh#CRGa&M-h6.mHDvd G?e TcA<C_THAzN`A\ğ@B$T d M_ZcTwHkD*,Ck&6F+Vk;*vkֶ%k覫+KZB2foVwR@!,*D HHȰÇ#JHŋ3jȱǏbɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵC^ÊKٳhӪ],۷pʝKnRvFū߿ LÈB0:!ǐ_q( fs䀹34>t=v['[<氞n=X; N#Z`Qa?Olv/ .A(z1 #0PaS$QJ"%4%>$@7IUuV@[D-$D9Q!2PAq,KnM`aE%dC1T AA$χ7ZD6Ђ*V x0W5A[A$E4đ ِ?q qQT#_V Jih ip)tiCeީ|矀ʔ瞃Xej袌6裐F*餔Vj饘f馜v!,*D HHȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿_*d X 68⅋;1lj-焐Sl3RG bX$kK"Sy㽁M N2Ut[h%Fr2 c&tf*H衈&袌6裐F*餔Vj饘B!,G*\ȰÇ#JHȑ C Hf˗0cõYq%ϟ@>)nʴiPZEH0`ʵ+h*ޒ+S3z+㕅%haܢTY+i"s ǁT ?X8x8L Yd"H$d"Ne|d.C+ZdR-V9Gp>/PX*()爆_٫o3 h.s@LTBS4B&e%q$kyB^AG_A5&H,F(VhfV!,.E H*\ȰÇ#2, ŋ3jȱG>Iɓ(S\ɲ˗0cʜI͛8sɳϟJѣH*rӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻxmt߿ LaBДBៅf X"E?k8!( .`_' a ,` l@ f9, T4脊oT"6A+D_+S#9bO5dN1!`{q>@~c-A6 B-DssAPU +&Y?|& `c1SS!HZg]R?CO\_4<h<P@cD?%R0>O0P?*tfs9) px}1^,X# '[.| $cz]@UXp Xn(PkGAa@֧z DA@5)B,BHEaO(Bm @ȶ>eA!>2nA?*l@s`4C1qK?P5|phMu e1P%};0ɰ QP'f@лIC Da5H|: =;AЎ:I-y)G<)ˤ!s& (#\3>8Y(`%ԣD#݂в? ?gHRA@ /T@k'mP)>/E-ТAF =fSe>PO2u9`S1P c?P\, =m؇I==@걄@TD!1K!8A`G P0!C# (8)kG: )|w!(ࡀ1ჁAMP4K E,|+E? @XaaQpt@n=Lf, iQ1P `L!@U@XˡZ@pf?A$ A IU N$5()=C b !B Z Ao@~(;Bq$u^@75H5`r`(?`xYȇ(d@0HA܀ ҡL`wlAx:j9]a`@O4 EE1 \AP %-{VVC @G 6GCsmH P'؀:a?WF=HpCF##] pgu!Ĉ, 1mHq $QK!Ȃ0 " (=֍$TjAR ibO H|`PxG1aOУ$P+5(@pQB|Ѝza+Vx1@EPϋL:V8,ESw8@SyNb|p a ܷ2B~Mq m R;}9o-OGZe*P>+~7·%GOOWֻgOϽw+嶾!,* H*\ȰÇw! 5Xh`@CdHfɓ(L" hICK6~y_, 2X2D+ʕru=]MZPACjOb[Pݔ2:#t w2J1T') =Hۑ>n}f8ϗj 軂DnHOᑁ6Ё cHT3WQ$heyRvj<2noDR P7E R@qVb<,V{9N 4@?"Ř#v/eS2lDO(d]{ $FE(MbS!6@+ |3UCiu@gT_D$KBH{ ۜq@ #@GZ ?P?'?#XB`=/lDEBeP/+bU0 DPy<?}?g0XrPDUT+ ,h 47Ӌ?@6B @a ʐ7#BP M95?AQO$A: s1C ؒjtcYhT>AP({y&ũ@D#@c:QB@0 H`L\$ P.51ې }yyd->4?Ah1Q\*Ԃ\1??+B|C?H@'C_ d@טsBV TI!пϴЃK >5/:B??MY?e6a;Kf/RE%Ќc zCAOu7 ґ57z GHB!%L Wx<0 gH8̡mhB@ H"(&:PH*ZX̢.z` H2hL6n1 !,!,,E H*\ȰÇ#Jxŋ3jHǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ ! '⃋Gf(EX%9y0G @PK)jt)|1 ;>A4P-5!04K%A+ p% @#0DrP( DʹPqy( 5m@ ?fhE0"s1 LA(D49 ( !p, " d3Aa r0R,Q>8FcA-Ih 1j'5()mXPjB6 PCѲ 1+P3 v?$E6JtBE0dmp 3 0ݩՖtpDkp;8B?YdR?WE@Hя@5S@N4J%E;mP9LaOpF& E4K]zTA~TvLc t?1r q|6mtim0<IN)-&\yؕUbVjE@-Ld 5s-=1R_#m% )#~Rfۮ;XbPڑ%o|G7!Wog =F!,." H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\YP ˗0cʜI͛8sn4@0rIѣ\>$Tb$\ ᗪk;ᧅ<ʴ4؁[W 3 c 0M U1LhnR&dy VFd3^5``d+L> 1 ąkoYv yE᥋ "h adD3qӦ#9y`лrx6;;"exp7A|4C%PA RRuU YB 6E=8dH9BZ=}SP-eaC`8Kw==` Ch H@ ba}H~_#Bps .C+@$G}4G2JAjD IiFЍ#ĝ HP 9!Ҟݘ Yj/Eّ1@C?y)G˲4B{b4kmsGSr:sM`>9$By0tADdjFw$Cvx ]bAlF#X#}k:glDca AQVSrIFDhW]6AtE($? 7w1L~,Ȇac (04l8< M DmH'L7!,-F H*\ȰÆJHqaĊ3jȱǏ C`ȓ(S\ɲ˗0cʜI͛8sɳϟ@ Jѣ8I"]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ[T@\0 )>@*!0yI/Wl+6*`muNXگDP$QfZZ1KIyಔ \`2PB 3:휬t #As?E Gwd@U?'!?SB #@_LKPX,Pp0Ѹ^BD%?-C8bqq76A'LA`4"K$x$JTC8 4EFƤ#FA[AOZq7%W"/+ }R D03ЊсRs @Z뜈 ; 04+:G420R¥bFBi8J"\lL1tPT)H UAu Cad4@bTCdє1^zJ;Qe0<)ヌ F' #nG,Non&11$+EP?!~O26U@rdTBZCvSK?<ɇP(18!#7QOH0 )xP7aR bp=S Bp x9r/{ HNFIy]O 8F'@@ur㔒iі `#A&BBjTCm5Pz]!pp8Jcٱ?DpR c8 qב 3=3161ZBV4B&$jЮBïH屮OS`X?c <б YX?< \.-)lK^X@Oh r#&bL No>!gBJ=Ua")6 \-y@3@( Bm3@D@J?QC?f('Yv"P=Ps.4F504GdC&4 E[9M0C'A\C[%CkTdBSctSh,b?6 &ӡG9aCqdHJ $JE >OL <hA BdD"C t ԗ )<s " ,P:!R" ѥ!хuP OM oBCʐ,?IkR3]3vFVh.hANd@TBhFy) ,A T{ uPoxc?EYFBpTK+ձѿg)stDxF@'&rR13DGM}Y!" XEi`Q@!,'8 H \ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@, ѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿"5/gI%d*,8lZFy*(ng冧 SVCZ5ՐGfG3$sT4Axf(1}P)TC.ENp?FTB4OO  Y-uAС L$AR@H=q(X}*lI k&SF+Vkfv!,'F H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷X\R0 +<6Q/E 8;(-2HZgH1G;̬L@kFf-e+2%{0?3uUZJ Aꟶ4d0c*J$RDKH)Wh9LycԂ$ ?tAn&QPiA3vJvE6?|2!S 4 Fr4[Fᓅ pQbF a0; ҎA88aJ8 "3P BGH})Pk`?А }63cUcRAh4ZMvn¹ TE|N$EngML ZOaT~A5z@r*Q,g(c͓*ѷ-1qRT7f Fa@SBGPࢡ@hApq@UD.HDDq?7*TG2$?O H)# ġ''m2fFwEA64pFܙLBCDB Bx -,*! )Ҏ] @OO HM|Ap4?&TTۮA-@LW1 QGK@6FȢ)MvY8A  rD(I4BD.#CʻMt@8 7" M@0 H(Њ<'?s>є ~RӋG(BOK"4@tC(Ug9qFUQ/QQ_ټ/bi`3BR ,ɢ I#mHQ4 >2ٓJO=a rD-iA4`@P+ Cy9Cv;pGA 䢎}#fMSs4BXViA @ 3p t$EFb+I%qgDFEC?ώd݊Fr?))s!,,@ X`*\ȰÇ#J ŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ ѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L:`(Å .P*g㗉%fJ? "J7oi~h/7('zjΦ!o"^G'6pEC4pSc, '4oӭ6%x;UP`s8r-  S 1A@)? y埡J8~.& h޿ ވv' NN:t5&ZH#jBVz!|(cL|ͿzW@|> :( D) bP> D@@QY|G>WEĄL(OGT I Q] 1P O# AQ3 21 hH@;5"UPvMB@E?'= . %yQ,y>)o6 &lD(cP! :/(P9!{ ijJei!p@AEOHt^PG$px6v.@ۈО 5! $ȉ~jy 3,1m".@w8$B4!O% -?Zd[I஄`(fH R>RG8DlRTPTTD V <ֈlMz,m0,Sl8<@RB[!,(0H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶-L * Rۻo6 kL/I<x'AX$h[3I-T#$ܖn5S HV۱ܪ;?Ё(r!BhTKIjB^ _ ";A4 }$ }GQ DCtdAC=F tKE 4]B?$=f(_F$?tDK >S/O- yPewA@?  pPl(+#AP2DJ@4?L@?L@5P>‚>mP|MQe 'x+㼈(#ӑN*#A>ԎAoHy7XQ8[L@bCB :]].-]XR/pBC e`96 mP)=>0AQ"y( _Ao,H-D :=cBX\<ACDtCIq j}!S==D%$]EldHpdf" q4vf@ pCFvAD BST4AhLT@AlO$ qS"/AfA ƒc98`]M=bL&DU4CTAHFg~DФeJj TbJ\LX Tg!ь]2AAD6Is҂SINQ D=BmTEHBRYlhl+P#k,F^$A]{g EIfv+(MKB!,*3 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0%I͛8sɳϟ@ JѣH*]ʴӧPJ`իXjʵׯ`ÊK)hӪ]˶۷pʝKݻx˷߿ ̴?Äi"k3CLCSqdY^m?/YrmPU쀳*3VV11nzGnL VRedRk:[?#0~*-"/Xb>dD49CQ! 96|@P=qR=YgPPY8p0 Q@ RP>OtD)dHQi(#f pPP3M$?|bE, IΜ RizYɐ@QFbqF Ua0;MD!\L'P 7T03v 0nj6ЋP.Y 4 @yѐ5S8(NSU`P@Eydh#@~(ĠGDDJF*?$T 6elDP:DU(з&UBFiF6TBw#@@.T?Ojv 7 WRg\w ,I!,"1 (`*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8s#@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߇N/ϼ0/ 4U/Bs;2attg%@YH1~.1h? M1 2ߨ *(I`qD HS+ʿy`:eGdHLuLIr@@6 HE CH;*.O}Q3cQPST0B?xOA#nFE$I4cDOѕdr8ɖ=4g* ,$V@!,+o *\ȰÇ#JHŋ3jȱ? 0౤IN\ɲ˄^le͛8odΟ ADtH送ЧTX]Fip?n fd󚕣CqDE ÖV;%/jKߨұUc䆋nf㌕Ib%QO\X+quчm1!w32^602 K0O5@׍# e,ߑqN'Ma/QhkCQ; @Q! Q+:4C} !G)OC@\׆ 5:G?$ϊ Cd4̈0SPLTGV51SMBa<+RbM4? ’BR)Qr&YR!E;.Pv|矀*蠄j袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ*무j뭸뮼+IJ@@!,$3 `*\ȰÇ#JHŋ3R,Ǐ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿` #Tܖ#. @ R.3-? p淏H0B2(рc~>Z^93 nQ- o`' 2 Iz0 Hzc,Ԧ{xj ?TEHѤ??ġ@Xa\GBvČD|Pc3҂(VP#Phg;5LQc7u#2i4?T-Ȑ @L@ @@c4& Ғ= DlH@AEB*bBcRTFEE64ad:#OHL`G> @E$?NBNJ!G@̈́iFEdLDIx"eGާ5jP.) Z"A RzTF'(dN=$5$#A,y"/BBJؖA&ߨAD$?\䁾%HS  cBD LG$CAB@G/D넍O$$0!*dxP+hУd3@С q < 15//a(2D.GCdt@Ő-=ћ }ݴ TA3[?T G--=,XF'A^Q y&MW&U@bWYIߑgXw=SB~/o>U9!,.9 H*\ȰÇ#JHŋ3jȱǏ C\`ɓ(S\ɲ˗0cʌh`͛8sʬϟ@ JCF*]ʴӧPJJիXjʵׯ`ÊKٳh"M˶۷pevݱu˷߿% LL A pL@(ſ;\p 'e 6 ݼ֫'A -/a @@y2Z LK uZ0>XB>! CP/0B D(j#Fr?iٲP^w3Au$So6A?\esM'9dB3G!#@P'21 MBqL@.lƉ!C/_ ? iF_D"I@C?X XKæ)m@9s1 (?F-(0U A?F ?@ت4cs0b@>7A) B0±@tD@(M (EW@X3@kC?@w2+x a ?bFBV 3H-򋈼Ars,Ml8,?8 |`. D X ^o 5z n {yBnfdp"ƸFL"D(޸*& ^_iBh`&@ 9&Qaʉ?x$RX@(vB6,I'<#"L#uՑIs@-o8F i$A@d0A@W"(%S1&BIfhp!<2)A  G2[$ĚID o! DlC| ZdAC5LTwG֡~CH[I H Q)3$3 Ć !9aKa 5+(Q4%?iHC1h#RdNr *b#A QOz91Pl~њ?3z9hA# ٢.]@ n1HGdE$Ar @ ?|x gD ԀDP:AjC HBetBD0tC4 AքBcT DzmQ#Pφo4@13 Ap>@R/J7#1P q؂C BE ST/G0<.i 0B~sQ:kM GXճo@Fx!mK 4EAXx4BH$ysy^^*,и0TP?'dDAÏH3RA8t@b rl'FVD@$.H\II#EL@tEbccK!"wB;Ћ5 Y)Q+lDQE UPP=˽E[ԛ@Χpdz$ RR7F dP>@x 'H ZT 7z GH(L W0 gh0!,.2 H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳ'E> JѣH*]4%ЦPJg`իP`ʵׯ`V5ٳ! ۷pʝK'ۺx˷oקy Z Lڗ\qCG)+ԀPH99Xp!SkSBҒA8{![ed|"mE Ia,Lb$#ADTxˏ1B4,4ExRt)k2$EhuB ɐ#ݱ&@,ZCQ=C ֏YD+aAK F*gA!4eE(AǤc %84A ZD=}(ҡ  GґMsy# 9@HІ :FUS# t qtD*d*F>uPKQv 'QScYlbdMJEoEw@[P2K)8WlqL_,њw챞,J( q,P.Cs47!,.= H*\ȰÇ#JHŋȱǏ CIR`(S\ɲ˗0cʜIƚ8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʅ huVٷӺ^̓0c:c?Vh)re!#`:f!0`yRv 4LQg9Kv!~twG}9N㜅Iޞbi ӿMĆW #OM-PڽF.PMt 8@ ,աWA (=Fu37ѠC tARC>nYWP8ePl8Y|Qņ ␄8"K8cNdc@SeVfZv`eddhl曋- tix矀*蠄j衈&袌6裐@e!,.F H*\ȰÇHŋȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ JѣH* `ӧPJJիXjZ4ׯ`ÊKٳ]˶۷pʝKBv˷߿ LaM+^/ބ~ܓ@ x#7$ gɍ: (O!6 S3mTZQ7:}m38AC#l1xBuSRzD]cMr ]ſX\p>4혼VP(@$Gc$TBJMY]ADPć@( FčCcG BBWkm SvfaR ̓2MBAE((FppH]?uohͳMբ,ܹv8Cn߀.n8e}7G.Wngޝ3w|O@!,.E H *\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8'Ph ϟ@ JѣH*=sӧPJJcϪXׯ`ÊKٳhӪ]˶۷pʝKݻm˷߿ LÈMLCY0>jr[ulj j6' (|$xz` FxKbȤK a_9N,-ZO):OްDc VZ`%\( 夒_SCABEBB0_CڑDDFSFFdHDDAHMX#2GCChtGQDęDrs!DT\G RE)MA KH6|CJ%u~Ph2xsуKT@ߵ\AѓЃ$uUPLӣGez`5dғs/(h$bEw^FAˀeiHX?OWs ? Q11YxJi,ĨC%'Gu&,G&[36FB<+HNkTA^ V y kf$nƛmk,l m*P@!,.E H A,Ç#JHŋ3jȱǏ Czd萤ȓ(S\ɲ˗0cʜI͛8sɳJ> JѣH*]ʴӧPJJUjݚ(ׯ`ÊKٳHIzE˶۷paKݻx˷߿ r.È+^̸+ HA  i(^;3/ P$2MU#{&Iba 6moYV;U_}fQqR9K~tWÁQ.kHhz@5uo>U oԿ)r Re u@ U]B4O,b QHqc? OGwh4EE$DAJ9 Q@9r@xGQa wgmqeQLёE#t@Ђn9IG$d:QBvtD򉑉4Bd@C@UpAPHBPDKҔ&9CBgz@](O#mEl$?@HDjoBII#x&@C?daӹEi a1ѵB@4D*QRrALQ=^Vf?z$,R<mH(~ Jt2 IP? "S F(d6 -cpJ?Zؐ: Le;Q/]rUtBu4ZpBC|`epO]nUR.N1ַ9t]~+ǾSU9d<׮J'{o37G/Wogwo@!, #9 H*\ȰÇ#JHŋ3jȱFCj< ɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷p- Qv˷߿ pB '%nN>D!%bK ˕iŽ1X(䜽r#J g"2H4$"`3lwh B[K /@Y} 3ͿڿP+#st(4!P"SniC=a9%G"x}5dOxͧ W>m`&q1ia{ @-0BU@1T BhFVOJrdO0hPyEC@0dHLA[C OB Qq/rQ g!fCHZiJ*4TjBxU1fPE T LE)8)C9ԥGOD@ydL=46ysQ+ѓDe> bgӕ֚Oup 45iG'!Al(͈ doG,Kn_w!,.D H Ç#JH@3jȱǏ C#ɓ(S\ɲ˗0cʜI͛8sɳgK> JѣH*]ʴӧPJJիXjʵׯ`v*ٳhӪ])ٶp 0Pܻx˷߿S\,C+^̸ǐ#KL^k,ܸ!Yv%}ꛘg ?Uv#CG M<:q S# @ E |z \Ab3) Vhi93HD@dFEv5<48G$TG2}#@c&vR(DZ Hus CƉrX f D-7gyђSAD(U)E $NFP Ouhh* fw3CEH MtCJ4IddG4<1D{:D HP3K[LFO0EKP: Te&Q! Y+5 "O@GB( 6 4OER<$HG$D9 E['@kLƑuCX|lC6D@,H|kP q@- > UPtCnժA)¢( Q aP&L!ь OtIATt@$1AtJ4A4CfH&?QT}OGd4Q~!O-4GFp FPDv PK E- B0$ J 4X򥂍l=է ?#@4CcAP$AiO.'?0G/|DCU^rTqD18A0}R4@I.SS\@ߍBP=q O-MUQqp 49@A8 1r.]"E\sdgP i91ZPp` P:@.$A IaNAhI:A/Ce/kDPr>Q1)Q{@13 4( C J$P/!P8%<%\F@ HC-0;99FȣkJn@T7PBO,X )ݐ!=m(LQ-m `  y0 #>#uG*C\-80plIP}G1 U @,(h Α94_9@}^A A#?@DUBA0?@z#KAJ8 A ⸊H7(r sI DLܦ,T3#<<gK1ʇrÙp`[f9M!D<D$q$[⮢ؘP!,)+ H*\ȰÇ#Jhŋ3jȑ# CĘj$&k侉Zʜ)͎Z!ϟ@ ZJD#t8N!ʤ @](d*XʵS^{(g*"ʱao8MA'J{TA@V%toYH{ ԺqB/#cPKsDٰſb%,QoG% ?|2@mC9npTmn )g׿|&]$9"ē;WA%t#P\H&oU2AP )4$ A(@Bu@œ3!AI4EF\!z%uL>@ %ODYP- zǝ8R6Md?e_G Fw؇Iz(4@mddDp 9s5]R;&]MG7DucApgI{|F蠄j衈d6IOBj饘f馜v騤.4iꪬ*hA*무j뮼+k&6F+VkfvzQ@!,,F H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴә JJիXjʵׯ`ÊKٳhӪ]˶mPnʝKݻx˷߿ .D|Bh?xπy RnXe(<ya{r4Hn,}uU>u"[xn).nJ;* ܤ0VB)slc~T(qC:dfIĂ EAAEf8DG]dC`KAF 4P' #}0&1 O@ф q#uCa>Ï? ãwA@( p Gu1cHCkFgg p-P4D#PtKK 4?.Qt -jP7mv&~]c 2\=p1PM? BdG*A-G_DAPu Y&(BaGQC Yd@te5XmB8dB7m8A=8͋FC "y!YF@i1LBy$J_t'COQ[!4M%2O )u 4l)<3a2,DmzIL7PG-TWmXg\w3!, ,5 H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhz<0r[qZv Vϼ0 {d2DZc[KpN+VJm}f-ֲibJ]X9:8M?F[C@%=S!,D h)"x>bw]3}ᧄi Y0lP aP  E#P;1' Rm#qH4 YaAWniS68=I @K7 ]MAPB$FEkTy8=Ԥ%K?C?06fE4KA!P7:?YB`DBP @-XQr\<ҏ BPMES AA$AӐ5A $DI$Lp4ѴVPeiMwS]iШp1F@fDԏ@dAB"A}]G4-FVٞqJO.JT$#qU>X%rX,Nv8<@-DmH'-J7PG-T!,,> H*\ȰÇ#JHŋLǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ v`,p^~&` 5#,R=}@^&Ic툂 nX "2Fw^;C@L(|L0L8c/l3=vPDѿg ddP-,Wlgn ,$l(,!,,5 H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ ` b&:!L03?<|> NZmkF\|C!+L`)}҆oY@g%ꂵ`tmGVM->Z6am|f`RrdS2SHP$ A2xHA8 PtE<)&PqЃ2&?䑏C E?mG &FH@+T!DC7RPbmݑi iP;=Q X' %@P=u1"H|tA!BrLdJP„iqK4GQ Y<Q!(ԁ5Ax#GD4iLq0PYCЄ5D,"SUJѭ#8liL@6A0|Jps-5F9%(TgP2{P"LD#F (wA !\LʐϐPGmV,ɋ\w`-dmQ!,,: H*\ȰÇ#JHŋA`ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿`  큃 XM0awr=1[^h,D!% \@~+ke)|=(2h75pGeQ, 6єANܤP?U a ADrBTqE4CdBIP$AL4G1Ac - >O=dFd,DG\taD^VAcD2,@HtB NA AsL$GInp2K-y ( DBAě@uNB?KBB(A"dBD.Е&xcmzB gBUCUCGۇ"4O-1W?BcG54EAVtQXl H@@Ou@ary#S>6 jx#Q'%)A ,Bq OycF` J!ys6*e}UePhcGTJEt`!CXKKX37sP~" DmH'L7PG-FMmXgZ!,)+ H*\ȰÇ#JHŋ3jh Ǐ CJL%$yL7ʗ07^i͍oϟ09`B!FcH&ӧOA*PPD m DjzKP9J4Yi\DI` ~3*>{:F1=5v V lP0X4:GO2r"!QC9bpSJM|QUbℇ*.x\[Mzx (>3:\%-RLDAPAOmP3 7qiuGHـ%OYG<NWLoEf3MD@]4F rEw ؇Hz(4F)F u4DD{ ?+lDs dFEpEIT?}ǒ0PxEF W|\)dilfFgtix|矀*蠄j衈&-裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ *무@@!,.7 H*\ȰÇ#JTX`ŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊK٥U<˶۷p`ݻx˷߿L ` 0 *nh@ ÄiBG:?}RRizٵk g,[lOEm#]g0:*u#0`?[Xkw $0Wa(c qCP a0a#\A2,nՑB?T J X1O) A rPiaP!H }SmF Q >XXKJ@@0!TUW P)ЌgPy? .1 {鰀pDCc#}#Ф !Y'@auc`Ai*TA4Aa ?(1`$Ahφ B4!X %&@`O+f<$DHjKaQaCZ@+ZEB6SKB䔖H ?;Q=  7Qp@sF!t 6\$6,v3|$F R` Ҽ \*d+P3 Pq[}# LWtD87e(d7sĩA#4dBBF#L@JbԪ$5Vii`a%E KdT =p?;tClT8A,?h6 ,#)E$@k @ 1S?[qM4?I)/},oI oFO~@!,.: H*\ȰÇ#JHŋ`|X`Ǐ CIɓ(S쨲˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJ`իXjʵׯ`ÊKٳhӪ]˶[,%[uݻx˗dܾ LÈ+mX傍 >/@ 3,⫵dNØ ,6F yζ9a4Օz}fEw'_=0:@dY}jYzi9 OFೃ02xtwtA=, PDIg@!QmS4C~T'C# "BǸi `}mO7cAXq4YDr^ADDHYxdBIt#BD3ʚsB AOltL D?g#E $CtBH DN9,4A,E[tJt3A՞A_ d-yF;$@r(4tB1U%T5T=Џ(uE:|4EB2,$-D0,@@KT#D1P:s7%"T6=Ф-բӟ\I?<+е҃E[Cݸ*PcPR5[fA?Z<%ђA[%mj`S 721%E2?Hڑc$4Fftx EVx=dX+6J>}pmx|߀.}M>.ካxGX@!,+9 H*\ȰÇ#JHŋA`ȱǏ CIɓ(X2˃^ʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]۵%۷pʝKݻxݫ߿ LÈ:#%c$3xYq9U)mr i}HFSjoMG!D?,DAE?`eM7LoJ}#16@L  @m.uC%%GDBrT(C\ .8 QjMpQ?e9C1UAW8$(Z&4J4G> ]џL''C:؀PӢ%fcr>% P P=i3 ,8=#mBLԂJq(AṰ*HLGB  m' u*[mA{8<@-DmH'(P_tHJyY\w`-dmhlvI!,.= H*\ȰÇ#JHŋ`ȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿K\ p*oCʘaya'Mfꏟ[9_@<RH-+S#H k}'o"gk: # y+L;  WOI%C# i* 1JGיOBLBHD SqE TO aP7 FYpdw!(DCpj-iO@S2 B;tBAFtCUC9TBZ =QGM i#ى@s EtBlؐq#DND[@DR2)P +~AR4]i\Њ<*2@TA^uJb54jA}_S VCШP=1S@X%0 Y!].E F* F2G?f&@kYT!PQP-|Ї25*HAGg(4O5)S.,4l8<@-DmH'L7=V@!,.4 H*\ȰÇ#JHE.jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXFͨׯ`ÊKV*ײh۷pʝKݻx˷߿ L`d""(LQ}m w8:fH&0K0L|`sB['הZ2>ZHᨆi-&)1HA&3iCVqAU aL|r1 /ԧD?dI,<1PbFh7RFl E 8=R,e ?8DaJ&ԀF18? At~!bEkR ЌѦ$BX8pG*r2e A Q І:  D@t BUIqPRbmMqQ;Q."0U7P3! M)Pg9g<S ACuZ@@ȟ 1YE #!4Ӑuٱ?`kN,(EjAP$-5cF.MݴBVpk+j̒l'F!,.: H*\ȰÇ#JHŋ`ȱǏ CIɓ()H!0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶Ǘn- ݻx˷߿ Lq.㍍%0!x0HRa&Zk%-q+ҬhM!qIh챰  $V׎1rcت3v>`}:GԑK˦!BJ>$ABL:-݂EO-m$uDCAQ9y!)%Ȑb MKAF@? t0It\BAADWB0G=4EMBX?(5D$C B <$?L4 AqLd8Czm$ѐ{FepF:CB4AA ٰ/]@ (KpBHAR,Њٓ*Ib,jMc DudGgE M Mx(KPh  :z"?Y$BtOZHH@*B.fPy*@xReBpT =,F#?GBiQ- (ِeLm Xl(]0,4gr8<^!,.9 X`*\ȰÇ#JHŋ3ZDǏ CIɓ(S`˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKb˲hӪ]˶۷pʝKݻx˷߿ La0@P` `@~ȶz8ag.d!s $L s3!?XV= AՏ&cLy[A@鞇rG],*r8@⭨?vXB;?(" A5saHA43?@\A|4=a%4xA@@aIDXdMB=:uP1DZJB|rs4D~T(c tA@A BtyO?I*A )- yL0 0J,C-K`MD $D yThCytBOTd_[CR tbFBNIO;̐)kB444B]ӟ0-+|[@4DU Pz dE>@ޮ775%\>{D8@wjC 4EB++@9pCK(@>@Tx;y G@ ?ר+ 1lPXBbB5$(M!Y~2#/K'*M{@C lTdOD6$AsϩئP>=x+DaCP7ܐۣ2Tg7HmQkxl?Cd?~. ВdkB,jDL 4 hЯ%Q E0:T @ի2WY$ )B,[4 #?~CBAa!u@$b)@ap3H$R&+:PS8H/@DK, HL&:/{"H'*ZX̢.z !, `*\ȰÇ#JHŋ3jȱǃ>*, InN\iK+^ib8s8nn|$i0B#>JʴD@guh8kmDځFk! Q-Gz2gB%Q5~ 3"15|}Ip:$drp GsWz[rQAE! G LXE2ҕHd3_H CH'5 |oDF m]: GC YD.nĮ*o10XϜHq*yVBwD@FΉR")hx:BuEQ~PR? DStRJRA wt&JVSsTT?!V+QALLf^mU!@ ay1Fэ Ffyw U" ydW ]' 9-!#~#DA*҇fJptFD@)?04H}Ȑu !8^:RR =>>2tf56 "Q9Q=9:e%]~dCg$@JCi@`:]) 褔VѠf*Q7h駠'!,  8@ *\ȰÇ#JHŋ%*ȱcB vIɌN\)QE@IfD<%4`˖!azQ,j;2S0C>zBRHa~,9h5,c>(҃Ķup U_;2b RRjr@`g9&0Cr U9 e%0P<')U! e`Qd#A IXD)UDbQFa? `TD J= /9Jq4pCG9 ݜ UER,$A`(REAdO!SE*EFPeD^I/Jl,4@iF)Q*0Q~4@ DT K:DȐ7#h$dDj(`MP٦[wMQּ+k 9%<],0Fi |Ұ' 7G,Wu^ESQ@!,.> H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷p9 *A 9@g-9!nLZҸ.w>ZiBd7ISXXfe:$ Yۘ9al#^C߿M-\vHA&DwK@c b = J`-EQ睖XS->A P{t aABB^E4@eIC  DG,D?D4A\sFC/D?!d@#L  P'94Gֽ AgbR7B}MAeQӊ[]ЎDу yPrP9u'_YvS@t Q͜$?b$B\4?D4AD?4E0)B,4St>J)D|GAdA=t@#@hP=I jQ EPR^XTA; 8)Pj:^C{qLʕ$IN (7 R@)DB Q C8![B$B\4? +4GLD AA@nl 7%}P QSP@mdCAV$DyCAJ X$=D 3ϙ&̐Vn  q` <ԧN$2J>x6ATA s-MO|VˌTOkn7 P; :E9R' B#T.%,E 5ZC4? $A o> RBhS@E1Q!?  ,RpLBSa}915%A$ALh 1ב>a{(#@mSOac} (8LƐG)F F((3PFV@9@N6^B>BILg71M iD9('Q6?d]I) rM(BJ ٰP/S#BC`Ep "F@ ?FJ$BDń y @Gm M1M3Зu%"Q* uِP8.O"C ]H@, JLՀIƔG?f`&xRxcҼ1v7Q-~^KQhA%0m$l&N,0,4l8<@-DmH'L7PG T@!,5 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx% b##xྕ}/8J^a)6I(Bz"Ia;LC?`4"A`3z8.%57pbB`Gb$? :9Qf*%Qo37Ma@Z)& `E2$dB}t@uUMtM(EPC=tATDD!CBG@,$A D4JBRPMЛA>P-&O;Ȑ-6A#=Y=!)-?Tid`PAA (-c"ke"BcӥHAeiЭ=184Bі]SauQ $J,vC$X$ՊEtjkJ @!,4 H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝK]Kq/A 0@0̋{ ao:zz8"J ?: s4qϿpNxre8a?k1RzzK}x N@4mxa,p@YB @t@ÃB0G0Hd#@(pK=qwÏ Q?eB= !)%B2!P# aP+"?+P0?D@B܅"P.{t B#ur@@?ڃ)Dc'MsSyŠ/7cAg@LtCȥhPPNt,B!#i#1P2-8/ 3LOxEjE  D@XgHy ?O?LXQ:-rHi)E628 C,@$$@.J1<<:בfFbbBHtF?T?@P'd;Bg,w^t/`Cwu'rB6gLp@0 DXSvaQI mB/,: tB|4jI@LD3B2KhQ\<GT "V$,:F!A385sDB$C: D-@$@7dA@V?q"t|DM EWM1 IBLXبH!, H*\ȰÇ#J8Eq\ CI`?.c4$󟝚.TΖ8 ѣ F)Au8:ИC%0Qe|ҰC;ԏ@QQŹOj ^C8a7`0 b.HoKߦ`{H,X" :;L4p$ $qKsU̅Qi$,#ҵA ny'1 mt@GCq!S"0ÿ=3:S}P)Hn!D^hT=8U3Kz#.JNd @ڈOHd !*CtPu^MO44@<=($?PdDeV>` - u_)哓?AHLԣIE@ 6P#P,aT.BEdLDLND[)UG>Le"H@J?<@T$k`#MMDPQI%54H?A, =h:KAN$p֑  $*`\ -iN3אf!x6z-zCrH ^X6X9XsfTVnBmQ0PDtDUD=@*٢NL D+Pd,FoI@X] С~ +Y` 9<<.,4']&8\S;Vi@-DFtLco-S@!,  H`-(ȰÇ#JlaŋUȱG> \ɲK:^ʜI#3:G_Hnj e(pጊDRWGڜP<C}tDdJ6ŔUR8F0D#-JAW3#F5PUӥ@gB4E4%5ϕBQQIO'ZkL8h;Wћ҃R:@[MqsG,7/}0Y(C-iTj+DԥKLvMĶkk6,A=d@;humanfriendly-4.18/docs/changelog.rst0000664000175000017500000000003613270176764020222 0ustar peterpeter00000000000000.. include:: ../CHANGELOG.rst humanfriendly-4.18/docs/conf.py0000664000175000017500000000442513223414325017030 0ustar peterpeter00000000000000# -*- coding: utf-8 -*- """Documentation build configuration file for the `humanfriendly` package.""" import os import sys # Add the 'humanfriendly' source distribution's root directory to the module path. sys.path.insert(0, os.path.abspath('..')) # -- General configuration ----------------------------------------------------- # Sphinx extension module names. extensions = [ 'sphinx.ext.doctest', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'humanfriendly.sphinx', ] # Configuration for the `autodoc' extension. autodoc_member_order = 'bysource' # Paths that contain templates, relative to this directory. templates_path = ['templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = u'humanfriendly' copyright = u'2018, Peter Odding' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # Find the package version and make it the release. from humanfriendly import __version__ as humanfriendly_version # noqa # The short X.Y version. version = '.'.join(humanfriendly_version.split('.')[:2]) # The full version, including alpha/beta/rc tags. release = humanfriendly_version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['build'] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Refer to the Python standard library. # From: http://twistedmatrix.com/trac/ticket/4582. intersphinx_mapping = dict( python2=('https://docs.python.org/2', None), python3=('https://docs.python.org/3', None), coloredlogs=('https://coloredlogs.readthedocs.io/en/latest/', None), ) # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'nature' humanfriendly-4.18/docs/index.rst0000664000175000017500000000124213270176764017402 0ustar peterpeter00000000000000humanfriendly: Human friendly input/output in Python ==================================================== Welcome to the documentation of `humanfriendly` version |release|! The following sections are available: .. contents:: :local: User documentation ------------------ The readme is the best place to start reading, it's targeted at all users and documents the command line interface: .. toctree:: readme.rst API documentation ----------------- The following API documentation is automatically generated from the source code: .. toctree:: api.rst Change log ---------- The change log lists notable changes to the project: .. toctree:: changelog.rst humanfriendly-4.18/docs/api.rst0000644000175000017500000000527713362535475017056 0ustar peterpeter00000000000000API documentation ================= The following API documentation was automatically generated from the source code of `humanfriendly` |release|: .. contents:: :local: A note about backwards compatibility ------------------------------------ The `humanfriendly` package started out as a single :mod:`humanfriendly` module. Eventually this module grew to a size that necessitated splitting up the code into multiple modules (see e.g. :mod:`~humanfriendly.tables`, :mod:`~humanfriendly.terminal`, :mod:`~humanfriendly.text` and :mod:`~humanfriendly.usage`). Most of the functionality that remains in the :mod:`humanfriendly` module will eventually be moved to submodules as well (as time permits and a logical subdivision of functionality presents itself to me). While moving functionality around like this my goal is to always preserve backwards compatibility. For example if a function is moved to a submodule an import of that function is added in the main module so that backwards compatibility with previously written import statements is preserved. If backwards compatibility of documented functionality has to be broken then the major version number will be bumped. So if you're using the `humanfriendly` package in your project, make sure to at least pin the major version number in order to avoid unexpected surprises. The :mod:`humanfriendly` module ------------------------------- .. automodule:: humanfriendly :members: The :mod:`humanfriendly.cli` module ----------------------------------- .. automodule:: humanfriendly.cli :members: The :mod:`humanfriendly.compat` module -------------------------------------- .. automodule:: humanfriendly.compat :members: The :mod:`humanfriendly.decorators` module ------------------------------------------ .. automodule:: humanfriendly.decorators :members: The :mod:`humanfriendly.prompts` module --------------------------------------- .. automodule:: humanfriendly.prompts :members: The :mod:`humanfriendly.sphinx` module -------------------------------------- .. automodule:: humanfriendly.sphinx :members: The :mod:`humanfriendly.tables` module -------------------------------------- .. automodule:: humanfriendly.tables :members: The :mod:`humanfriendly.terminal` module ---------------------------------------- .. automodule:: humanfriendly.terminal :members: The :mod:`humanfriendly.testing` module --------------------------------------- .. automodule:: humanfriendly.testing :members: The :mod:`humanfriendly.text` module ------------------------------------ .. automodule:: humanfriendly.text :members: The :mod:`humanfriendly.usage` module ------------------------------------- .. automodule:: humanfriendly.usage :members: humanfriendly-4.18/docs/readme.rst0000664000175000017500000000003313270176764017525 0ustar peterpeter00000000000000.. include:: ../README.rst humanfriendly-4.18/humanfriendly.egg-info/0000755000175000017500000000000013433604174021137 5ustar peterpeter00000000000000humanfriendly-4.18/humanfriendly.egg-info/PKG-INFO0000664000175000017500000002240613433604174022242 0ustar peterpeter00000000000000Metadata-Version: 1.1 Name: humanfriendly Version: 4.18 Summary: Human friendly output for text interfaces using Python Home-page: https://humanfriendly.readthedocs.io Author: Peter Odding Author-email: peter@peterodding.com License: MIT Description: humanfriendly: Human friendly input/output in Python ==================================================== .. image:: https://travis-ci.org/xolox/python-humanfriendly.svg?branch=master :target: https://travis-ci.org/xolox/python-humanfriendly .. image:: https://coveralls.io/repos/xolox/python-humanfriendly/badge.png?branch=master :target: https://coveralls.io/r/xolox/python-humanfriendly?branch=master The functions and classes in the `humanfriendly` package can be used to make text interfaces more user friendly. Some example features: - Parsing and formatting numbers, file sizes, pathnames and timespans in simple, human friendly formats. - Easy to use timers for long running operations, with human friendly formatting of the resulting timespans. - Prompting the user to select a choice from a list of options by typing the option's number or a unique substring of the option. - Terminal interaction including text styling (ANSI escape sequences), user friendly rendering of usage messages and querying the terminal for its size. The `humanfriendly` package is currently tested on Python 2.6, 2.7, 3.4, 3.5, 3.6, 3.7 and PyPy (2.7) on Linux and Mac OS X. While the intention is to support Windows as well, you may encounter some rough edges. .. contents:: :local: Getting started --------------- It's very simple to start using the `humanfriendly` package:: >>> import humanfriendly >>> user_input = raw_input("Enter a readable file size: ") Enter a readable file size: 16G >>> num_bytes = humanfriendly.parse_size(user_input) >>> print num_bytes 16000000000 >>> print "You entered:", humanfriendly.format_size(num_bytes) You entered: 16 GB >>> print "You entered:", humanfriendly.format_size(num_bytes, binary=True) You entered: 14.9 GiB Command line ------------ .. A DRY solution to avoid duplication of the `humanfriendly --help' text: .. .. [[[cog .. from humanfriendly.usage import inject_usage .. inject_usage('humanfriendly.cli') .. ]]] **Usage:** `humanfriendly [OPTIONS]` Human friendly input/output (text formatting) on the command line based on the Python package with the same name. **Supported options:** .. csv-table:: :header: Option, Description :widths: 30, 70 "``-c``, ``--run-command``","Execute an external command (given as the positional arguments) and render a spinner and timer while the command is running. The exit status of the command is propagated." ``--format-table``,"Read tabular data from standard input (each line is a row and each whitespace separated field is a column), format the data as a table and print the resulting table to standard output. See also the ``--delimiter`` option." "``-d``, ``--delimiter=VALUE``","Change the delimiter used by ``--format-table`` to ``VALUE`` (a string). By default all whitespace is treated as a delimiter." "``-l``, ``--format-length=LENGTH``","Convert a length count (given as the integer or float ``LENGTH``) into a human readable string and print that string to standard output." "``-n``, ``--format-number=VALUE``","Format a number (given as the integer or floating point number ``VALUE``) with thousands separators and two decimal places (if needed) and print the formatted number to standard output." "``-s``, ``--format-size=BYTES``","Convert a byte count (given as the integer ``BYTES``) into a human readable string and print that string to standard output." "``-b``, ``--binary``","Change the output of ``-s``, ``--format-size`` to use binary multiples of bytes (base-2) instead of the default decimal multiples of bytes (base-10)." "``-t``, ``--format-timespan=SECONDS``","Convert a number of seconds (given as the floating point number ``SECONDS``) into a human readable timespan and print that string to standard output." ``--parse-length=VALUE``,"Parse a human readable length (given as the string ``VALUE``) and print the number of metres to standard output." ``--parse-size=VALUE``,"Parse a human readable data size (given as the string ``VALUE``) and print the number of bytes to standard output." ``--demo``,"Demonstrate changing the style and color of the terminal font using ANSI escape sequences." "``-h``, ``--help``",Show this message and exit. .. [[[end]]] A note about size units ----------------------- When I originally published the `humanfriendly` package I went with binary multiples of bytes (powers of two). It was pointed out several times that this was a poor choice (see issue `#4`_ and pull requests `#8`_ and `#9`_) and thus the new default became decimal multiples of bytes (powers of ten): +------+---------------+---------------+ | Unit | Binary value | Decimal value | +------+---------------+---------------+ | KB | 1024 | 1000 + +------+---------------+---------------+ | MB | 1048576 | 1000000 | +------+---------------+---------------+ | GB | 1073741824 | 1000000000 | +------+---------------+---------------+ | TB | 1099511627776 | 1000000000000 | +------+---------------+---------------+ | etc | | | +------+---------------+---------------+ The option to use binary multiples of bytes remains by passing the keyword argument `binary=True` to the `format_size()`_ and `parse_size()`_ functions. Contact ------- The latest version of `humanfriendly` is available on PyPI_ and GitHub_. The documentation is hosted on `Read the Docs`_ and includes a changelog_. For bug reports please create an issue on GitHub_. If you have questions, suggestions, etc. feel free to send me an e-mail at `peter@peterodding.com`_. License ------- This software is licensed under the `MIT license`_. © 2018 Peter Odding. .. External references: .. _#4: https://github.com/xolox/python-humanfriendly/issues/4 .. _#8: https://github.com/xolox/python-humanfriendly/pull/8 .. _#9: https://github.com/xolox/python-humanfriendly/pull/9 .. _changelog: https://humanfriendly.readthedocs.io/en/latest/changelog.html .. _format_size(): https://humanfriendly.readthedocs.io/en/latest/#humanfriendly.format_size .. _GitHub: https://github.com/xolox/python-humanfriendly .. _MIT license: http://en.wikipedia.org/wiki/MIT_License .. _parse_size(): https://humanfriendly.readthedocs.io/en/latest/#humanfriendly.parse_size .. _peter@peterodding.com: peter@peterodding.com .. _PyPI: https://pypi.python.org/pypi/humanfriendly .. _Read the Docs: https://humanfriendly.readthedocs.io Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Environment :: Console Classifier: Framework :: Sphinx :: Extension Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Natural Language :: English Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Communications Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: System :: Shells Classifier: Topic :: System :: System Shells Classifier: Topic :: System :: Systems Administration Classifier: Topic :: Terminals Classifier: Topic :: Text Processing :: General Classifier: Topic :: Text Processing :: Linguistic Classifier: Topic :: Utilities humanfriendly-4.18/humanfriendly.egg-info/entry_points.txt0000664000175000017500000000007213433604174024436 0ustar peterpeter00000000000000[console_scripts] humanfriendly = humanfriendly.cli:main humanfriendly-4.18/humanfriendly.egg-info/SOURCES.txt0000664000175000017500000000164413433604174023032 0ustar peterpeter00000000000000CHANGELOG.rst LICENSE.txt MANIFEST.in README.rst constraints.txt requirements-checks.txt requirements-tests.txt requirements-travis.txt setup.cfg setup.py docs/api.rst docs/changelog.rst docs/conf.py docs/index.rst docs/readme.rst docs/images/ansi-demo.png docs/images/html-to-ansi.png docs/images/pretty-table.png docs/images/spinner-basic.gif docs/images/spinner-with-progress.gif docs/images/spinner-with-timer.gif humanfriendly/__init__.py humanfriendly/cli.py humanfriendly/compat.py humanfriendly/decorators.py humanfriendly/prompts.py humanfriendly/sphinx.py humanfriendly/tables.py humanfriendly/terminal.py humanfriendly/testing.py humanfriendly/tests.py humanfriendly/text.py humanfriendly/usage.py humanfriendly.egg-info/PKG-INFO humanfriendly.egg-info/SOURCES.txt humanfriendly.egg-info/dependency_links.txt humanfriendly.egg-info/entry_points.txt humanfriendly.egg-info/requires.txt humanfriendly.egg-info/top_level.txthumanfriendly-4.18/humanfriendly.egg-info/dependency_links.txt0000664000175000017500000000000113433604174025207 0ustar peterpeter00000000000000 humanfriendly-4.18/humanfriendly.egg-info/requires.txt0000664000175000017500000000040413433604174023537 0ustar peterpeter00000000000000 [:python_version == "2.6" or python_version == "2.7" or python_version == "3.0" or python_version == "3.1" or python_version == "3.2"] monotonic [:python_version == "2.6" or python_version == "3.0"] importlib unittest2 [:sys_platform == "win32"] pyreadline humanfriendly-4.18/humanfriendly.egg-info/top_level.txt0000664000175000017500000000001613433604174023670 0ustar peterpeter00000000000000humanfriendly humanfriendly-4.18/CHANGELOG.rst0000644000175000017500000011414613433604150016622 0ustar peterpeter00000000000000Changelog ========= The purpose of this document is to list all of the notable changes to this project. The format was inspired by (but doesn't strictly adhere to) `Keep a Changelog`_ . This project adheres to `semantic versioning`_. .. contents:: :local: .. _Keep a Changelog: http://keepachangelog.com/ .. _semantic versioning: http://semver.org/ `Release 4.18`_ (2019-02-21) ---------------------------- - Added ``humanfriendly.text.generate_slug()`` function. - Fixed "invalid escape sequence" DeprecationWarning (pointed out by Python >= 3.6). - Fought Travis CI (for way too long) in order to restore Python 2.6, 2.7, 3.4, 3.5, 3.6 and 3.7 compatibility in the Travis CI configuration (unrelated to the ``humanfriendly`` package itself). .. _Release 4.18: https://github.com/xolox/python-humanfriendly/compare/4.17...4.18 `Release 4.17`_ (2018-10-20) ---------------------------- - Add Python 3.7 to versions tested on Travis CI and using ``tox`` and document compatibility with Python 3.7. - Add rudimentary caching decorator for functions: Over the years I've used several variations on this function in multiple projects and I'd like to consolidate all of those implementations into a single one that's properly tested and documented. Due to the simplicity and lack of external dependencies it seemed kind of fitting to include this in the ``humanfriendly`` package, which has become a form of extended standard library for my Python projects 😇. .. _Release 4.17: https://github.com/xolox/python-humanfriendly/compare/4.16.1...4.17 `Release 4.16.1`_ (2018-07-21) ------------------------------ Yet another ANSI to HTML improvement: Emit an ANSI reset code before emitting ANSI escape sequences that change styles, so that previously activated styles don't inappropriately "leak through" to the text that follows. .. _Release 4.16.1: https://github.com/xolox/python-humanfriendly/compare/4.16...4.16.1 `Release 4.16`_ (2018-07-21) ---------------------------- More HTML to ANSI improvements: - Added ``humanfriendly.text.compact_empty_lines()`` function. - Enable optional ``html_to_ansi(data[, callback])`` argument. - Added a code sample and screenshot to the ``HTMLConverter`` documentation. - Emit vertical whitespace for block tags like ``
``, ``

`` and ``

``
  and post-process the generated output in ``__call__()`` to compact empty lines.
- Don't pre-process preformatted text using the user defined text callback.
- Improve robustness against malformed HTML (previously an ``IndexError`` would
  be raised when a closing ```` tag was encountered without a corresponding
  opening ```` tag).
- Emit an ANSI reset code when ``HTMLConverter.close()`` is called and a style
  is still active (improves robustness against malformed HTML).

.. _Release 4.16: https://github.com/xolox/python-humanfriendly/compare/4.15.1...4.16

`Release 4.15.1`_ (2018-07-14)
------------------------------

Bug fixes for HTML to ANSI conversion.

HTML entities were being omitted from conversion because I had neglected to
define the ``handle_charref()`` and ``handle_entityref()`` methods (whose
definitions are so conveniently given in the documentation of the
``HTMLParser`` class 😇).

.. _Release 4.15.1: https://github.com/xolox/python-humanfriendly/compare/4.15...4.15.1

`Release 4.15`_ (2018-07-14)
----------------------------

Added the ``ansi_to_html()`` function which is a shortcut for the
``HTMLConverter`` class that's based on ``html.parser.HTMLParser``.

This new functionality converts HTML with simple text formatting tags like
```` for bold, ```` for italic, ```` for underline, ```` for
colors, etc. to text with ANSI escape sequences.

I'm still working on that awesome new project (update: see chat-archive_), this
functionality was born there but seemed like a useful addition to the
``humanfriendly`` package, given the flexibility that this provides 😇.

.. _Release 4.15: https://github.com/xolox/python-humanfriendly/compare/4.14...4.15

`Release 4.14`_ (2018-07-13)
----------------------------

Support for 24-bit (RGB) terminal colors. Works by accepting a tuple or
list with three integers representing an RGB (red, green, blue) color.

.. _Release 4.14: https://github.com/xolox/python-humanfriendly/compare/4.13...4.14

`Release 4.13`_ (2018-07-09)
----------------------------

Support for *italic* text rendering on the terminal.

I'm working on an awesome new project (update: see chat-archive_) that's almost
ready to publish, but then I noticed that I couldn't render italic text on the
terminal using the humanfriendly package. I checked and sure enough my terminal
supported it just fine, so I didn't see any reason not to fix this now 😇.

.. _Release 4.13: https://github.com/xolox/python-humanfriendly/compare/4.12.1...4.13
.. _chat-archive: https://chat-archive.readthedocs.io/

`Release 4.12.1`_ (2018-05-10)
------------------------------

It was reported in issue `#28`_ that ``humanfriendly --demo`` didn't work
on Python 3 due to two unrelated ``TypeError`` exceptions. First I added
a failing regression test to the test suite (`here's the failing build
`_)
and then I applied the changes suggested in issue `#28`_, confirming that both
issues are indeed fixed because the test now passes (`here's the successful
build `_).

.. _Release 4.12.1: https://github.com/xolox/python-humanfriendly/compare/4.12...4.12.1
.. _#28: https://github.com/xolox/python-humanfriendly/issues/28

`Release 4.12`_ (2018-04-26)
----------------------------

- Make ``format_timespan()`` accept ``datetime.timedelta`` objects (fixes `#27`_).
- Add ``license`` key to ``setup.py`` script (pointed out to me in `coloredlogs
  pull request #53 `_).

.. _Release 4.12: https://github.com/xolox/python-humanfriendly/compare/4.11...4.12
.. _#27: https://github.com/xolox/python-humanfriendly/issues/27

`Release 4.11`_ (2018-04-26)
----------------------------

Added this changelog as requested in `#23`_.

I've held off on having to keep track of changelogs in my open source
programming projects until now (2018) because it's yet another piece of
bookkeeping that adds overhead to project maintenance versus just writing the
damn code and throwing it up on GitHub :-p. However all that time I felt bad
for not publishing change logs and I knew that requests would eventually come
in and indeed in the past months I've received two requests in `#23`_ and in
`issue #55 of coloredlogs `_.

I actually wrote a Python script that uses the ``git tag`` and ``git
for-each-ref`` commands to automatically generate a ``CHANGELOG.rst``
"prototype" (requiring manual editing to clean it up) to bootstrap the contents
of this document. I'm tempted to publish that now but don't want to get
sidetracked even further :-).

.. _Release 4.11: https://github.com/xolox/python-humanfriendly/compare/4.10...4.11
.. _#23: https://github.com/xolox/python-humanfriendly/issues/23

`Release 4.10`_ (2018-03-31)
----------------------------

Added the ``Timer.sleep()`` method to sleep "no more than" the given number of seconds.

.. _Release 4.10: https://github.com/xolox/python-humanfriendly/compare/4.9...4.10

`Release 4.9`_ (2018-03-28)
---------------------------

Added the ``format_rst_table()`` function to render RST (reStructuredText) tables.

.. _Release 4.9: https://github.com/xolox/python-humanfriendly/compare/4.8...4.9

`Release 4.8`_ (2018-01-20)
---------------------------

Added the ``coerce_pattern()`` function. I previously created this for
vcs-repo-mgr_ and now need the same thing in qpass_ so I'm putting it in
humanfriendly :-) because it kind of fits with the other coercion functions.

.. _Release 4.8: https://github.com/xolox/python-humanfriendly/compare/4.7...4.8
.. _vcs-repo-mgr: https://vcs-repo-mgr.readthedocs.io/
.. _qpass: https://qpass.readthedocs.io/

`Release 4.7`_ (2018-01-14)
---------------------------

- Added support for background colors and 256 color mode (related to `issue 35 on the coloredlogs issue tracker `_).
- Added tests for ``output()``, ``message()`` and ``warning()``.

.. _Release 4.7: https://github.com/xolox/python-humanfriendly/compare/4.6...4.7

`Release 4.6`_ (2018-01-04)
---------------------------

Fixed issue #21 by implementing support for bright (high intensity) terminal colors.

.. _Release 4.6: https://github.com/xolox/python-humanfriendly/compare/4.5...4.6
.. _#21: https://github.com/xolox/python-humanfriendly/issues/21

`Release 4.5`_ (2018-01-04)
---------------------------

Fixed issue `#16` by merging pull request `#17`_: Extend byte ranges, add RAM
output to command line.

In the merge commit I removed the ``--format-bytes`` option that `#17`_ added
and instead implemented a ``--binary`` option which changes ``--format-size``
to use binary multiples of bytes (base-2) instead of decimal multiples of bytes
(base-10).

.. _Release 4.5: https://github.com/xolox/python-humanfriendly/compare/4.4.2...4.5
.. _#16: https://github.com/xolox/python-humanfriendly/issues/16
.. _#17: https://github.com/xolox/python-humanfriendly/pulls/17

`Release 4.4.2`_ (2018-01-04)
-----------------------------

- Fixed ``ImportError`` exception on Windows due to interactive prompts (fixes `#19`_ by merging `#20`_.).
- Enable MacOS builds on Travis CI and document MacOS compatibility.
- Change Sphinx documentation theme.

.. _Release 4.4.2: https://github.com/xolox/python-humanfriendly/compare/4.4.1...4.4.2
.. _#19: https://github.com/xolox/python-humanfriendly/issues/19
.. _#20: https://github.com/xolox/python-humanfriendly/pull/20

`Release 4.4.1`_ (2017-08-07)
-----------------------------

Include the Sphinx documentation in source distributions (same rationales as
for the similar change made to 'coloredlogs' and 'verboselogs').

.. _Release 4.4.1: https://github.com/xolox/python-humanfriendly/compare/4.4...4.4.1

`Release 4.4`_ (2017-07-16)
---------------------------

Added the ``make_dirs()`` and ``touch()`` functions to the ``humanfriendly.testing`` module.

.. _Release 4.4: https://github.com/xolox/python-humanfriendly/compare/4.3...4.4

`Release 4.3`_ (2017-07-10)
---------------------------

Don't log duplicate output in ``run_cli()``.

.. _Release 4.3: https://github.com/xolox/python-humanfriendly/compare/4.2...4.3

`Release 4.2`_ (2017-07-10)
---------------------------

Automatically reconfigure logging in ``run_cli()``.

.. _Release 4.2: https://github.com/xolox/python-humanfriendly/compare/4.1...4.2

`Release 4.1`_ (2017-07-10)
---------------------------

Improve ``run_cli()`` to always log standard error as well.

.. _Release 4.1: https://github.com/xolox/python-humanfriendly/compare/4.0...4.1

`Release 4.0`_ (2017-07-10)
---------------------------

Backwards incompatible improvements to ``humanfriendly.testing.run_cli()``.

I just wasted quite a bit of time debugging a Python 3.6 incompatibility in
deb-pkg-tools (see build 251688788_) which was obscured by my naive
implementation of the ``run_cli()`` function. This change is backwards
incompatible because ``run_cli()`` now intercepts all exceptions whereas
previously it would only intercept ``SystemExit``.

.. _Release 4.0: https://github.com/xolox/python-humanfriendly/compare/3.8...4.0
.. _251688788: https://travis-ci.org/xolox/python-deb-pkg-tools/builds/251688788

`Release 3.8`_ (2017-07-02)
---------------------------

Make it easy to mock the ``$HOME`` directory.

.. _Release 3.8: https://github.com/xolox/python-humanfriendly/compare/3.7...3.8

`Release 3.7`_ (2017-07-01)
---------------------------

Enable customizable skipping of tests.

.. _Release 3.7: https://github.com/xolox/python-humanfriendly/compare/3.6.1...3.7

`Release 3.6.1`_ (2017-06-24)
-----------------------------

Improved the robustness of the ``PatchedAttribute`` and ``PatchedItem`` classes.

.. _Release 3.6.1: https://github.com/xolox/python-humanfriendly/compare/3.6...3.6.1

`Release 3.6`_ (2017-06-24)
---------------------------

- Made the retry limit in interactive prompts configurable.
- Refactored the makefile and Travis CI configuration.

.. _Release 3.6: https://github.com/xolox/python-humanfriendly/compare/3.5...3.6

`Release 3.5`_ (2017-06-24)
---------------------------

Added ``TestCase.assertRaises()`` enhancements.

.. _Release 3.5: https://github.com/xolox/python-humanfriendly/compare/3.4.1...3.5

`Release 3.4.1`_ (2017-06-24)
-----------------------------

Bug fix for Python 3 syntax incompatibility.

.. _Release 3.4.1: https://github.com/xolox/python-humanfriendly/compare/3.4...3.4.1

`Release 3.4`_ (2017-06-24)
---------------------------

Promote the command line testing function to the public API.

.. _Release 3.4: https://github.com/xolox/python-humanfriendly/compare/3.3...3.4

`Release 3.3`_ (2017-06-24)
---------------------------

- Added the ``humanfriendly.text.random_string()`` function.
- Added the ``humanfriendly.testing`` module with unittest helpers.
- Define ``humanfriendly.text.__all__``.

.. _Release 3.3: https://github.com/xolox/python-humanfriendly/compare/3.2...3.3

`Release 3.2`_ (2017-05-18)
---------------------------

Added the ``humanfriendly.terminal.output()`` function to auto-encode terminal
output to avoid encoding errors and applied the use of this function in various
places throughout the package.

.. _Release 3.2: https://github.com/xolox/python-humanfriendly/compare/3.1...3.2

`Release 3.1`_ (2017-05-06)
---------------------------

Improved usage message parsing and rendering.

While working on a new project I noticed that the ``join_lines()`` call in
``render_usage()`` could corrupt lists as observed here:

https://github.com/xolox/python-rsync-system-backup/blob/ed73787745e706cb6ab76c73acb2480e24d87d7b/README.rst#command-line (check the part after 'Supported locations include:')

To be honest I'm not even sure why I added that ``join_lines()`` call to begin
with and I can't think of any good reasons to keep it there, so gone it is!

.. _Release 3.1: https://github.com/xolox/python-humanfriendly/compare/3.0...3.1

`Release 3.0`_ (2017-05-04)
---------------------------

- Added support for min, mins abbreviations for minutes based on `#14`_.
- Added Python 3.6 to supported versions on Travis CI and in documentation.

I've decided to bump the major version number after merging pull request `#14`_
because the ``humanfriendly.time_units`` data structure was changed. Even
though this module scope variable isn't included in the online documentation,
nothing stops users from importing it anyway, so this change is technically
backwards incompatible. Besides, version numbers are cheap. In fact, they are
infinite! :-)

.. _Release 3.0: https://github.com/xolox/python-humanfriendly/compare/2.4...3.0
.. _#14: https://github.com/xolox/python-humanfriendly/pull/14

`Release 2.4`_ (2017-02-14)
---------------------------

Make ``usage()`` and ``show_pager()`` more user friendly by changing how
``less`` as a default pager is invoked (with specific options).

.. _Release 2.4: https://github.com/xolox/python-humanfriendly/compare/2.3.2...2.4

`Release 2.3.2`_ (2017-01-17)
-----------------------------

Bug fix: Don't hard code conditional dependencies in wheels.

.. _Release 2.3.2: https://github.com/xolox/python-humanfriendly/compare/2.3.1...2.3.2

`Release 2.3.1`_ (2017-01-17)
-----------------------------

Fix ``parse_usage()`` tripping up on commas in option labels.

.. _Release 2.3.1: https://github.com/xolox/python-humanfriendly/compare/2.3...2.3.1

`Release 2.3`_ (2017-01-16)
---------------------------

- Switch to monotonic clock for timers based on `#13`_.
- Change ``readthedocs.org`` to ``readthedocs.io`` everywhere.
- Improve intersphinx references in documentation.
- Minor improvements to setup script.

.. _Release 2.3: https://github.com/xolox/python-humanfriendly/compare/2.2.1...2.3
.. _#13: https://github.com/xolox/python-humanfriendly/issues/13

`Release 2.2.1`_ (2017-01-10)
-----------------------------

- Improve use of timers as context managers by returning the timer object (as originally intended).
- Minor improvements to reStructuredText formatting in various docstrings.

.. _Release 2.2.1: https://github.com/xolox/python-humanfriendly/compare/2.2...2.2.1

`Release 2.2`_ (2016-11-30)
---------------------------

- Fix and add a test for ``parse_date()`` choking on Unicode strings.
- Only use "readline hints" in prompts when standard input is a tty.

.. _Release 2.2: https://github.com/xolox/python-humanfriendly/compare/2.1...2.2

`Release 2.1`_ (2016-10-09)
---------------------------

Added ``clean_terminal_output()`` function to sanitize captured terminal output.

.. _Release 2.1: https://github.com/xolox/python-humanfriendly/compare/2.0.1...2.1

`Release 2.0.1`_ (2016-09-29)
-----------------------------

Update ``README.rst`` based on the changes in 2.0 by merging `#12`_.

.. _Release 2.0.1: https://github.com/xolox/python-humanfriendly/compare/2.0...2.0.1
.. _#12: https://github.com/xolox/python-humanfriendly/pull/12

`Release 2.0`_ (2016-09-29)
---------------------------

Proper support for IEEE 1541 definitions of units (fixes `#4`_, merges `#8`_ and `#9`_).

.. _Release 2.0: https://github.com/xolox/python-humanfriendly/compare/1.44.9...2.0
.. _#4: https://github.com/xolox/python-humanfriendly/issues/4
.. _#8: https://github.com/xolox/python-humanfriendly/pull/8
.. _#9: https://github.com/xolox/python-humanfriendly/pull/9

`Release 1.44.9`_ (2016-09-28)
------------------------------

- Fix and add tests for the timespan formatting issues reported in issues `#10`_ and `#11`_.
- Refactor ``Makefile``, switch to ``py.test``, add wheel support, etc.

.. _#10: https://github.com/xolox/python-humanfriendly/issues/10
.. _#11: https://github.com/xolox/python-humanfriendly/issues/11
.. _Release 1.44.9: https://github.com/xolox/python-humanfriendly/compare/1.44.8...1.44.9

`Release 1.44.8`_ (2016-09-28)
------------------------------

- Fixed `issue #7`_ (``TypeError`` when calling ``show_pager()`` on Python 3) and added a test.
- Minor improvements to the ``setup.py`` script.
- Stop testing tags on Travis CI.

.. _Release 1.44.8: https://github.com/xolox/python-humanfriendly/compare/1.44.7...1.44.8
.. _issue #7: https://github.com/xolox/python-humanfriendly/issues/7

`Release 1.44.7`_ (2016-04-21)
------------------------------

Minor improvements to usage message reformatting.

.. _Release 1.44.7: https://github.com/xolox/python-humanfriendly/compare/1.44.6...1.44.7

`Release 1.44.6`_ (2016-04-21)
------------------------------

Remove an undocumented ``.strip()`` call  from ``join_lines()``.

Why I noticed this: It has the potential to eat significant white
space in usage messages that are marked up in reStructuredText syntax.

Why I decided to change it: The behavior isn't documented and on
second thought I wouldn't expect a function called ``join_lines()``
to strip any and all leading/trailing white space.

.. _Release 1.44.6: https://github.com/xolox/python-humanfriendly/compare/1.44.5...1.44.6

`Release 1.44.5`_ (2016-03-20)
------------------------------

Improved the usage message parsing algorithm (also added a proper test). Refer
to ``test_parse_usage_tricky()`` for an example of a usage message that is now
parsed correctly but would previously confuse the dumb "parsing" algorithm in
``parse_usage()``.

.. _Release 1.44.5: https://github.com/xolox/python-humanfriendly/compare/1.44.4...1.44.5

`Release 1.44.4`_ (2016-03-15)
------------------------------

Made usage message parsing a bit more strict. Admittedly this still needs a lot
more love to make it more robust but I lack the time to implement this at the
moment. Some day soon! :-)

.. _Release 1.44.4: https://github.com/xolox/python-humanfriendly/compare/1.44.3...1.44.4

`Release 1.44.3`_ (2016-02-20)
------------------------------

Unbreak conditional importlib dependency after breakage observed here:
https://travis-ci.org/xolox/python-humanfriendly/builds/110585766

.. _Release 1.44.3: https://github.com/xolox/python-humanfriendly/compare/1.44.2...1.44.3

`Release 1.44.2`_ (2016-02-20)
------------------------------

- Make conditional importlib dependency compatible with wheels: While running
  tox tests of another project of mine that uses the humanfriendly package I
  noticed a traceback when importing the humanfriendly package (because
  importlib was missing). After some digging I found that tox uses pip to
  install packages and pip converts source distributions to wheel distributions
  before/during installation, thereby dropping the conditional importlib
  dependency.

- Added the Sphinx extension trove classifier to the ``setup.py`` script.

.. _Release 1.44.2: https://github.com/xolox/python-humanfriendly/compare/1.44.1...1.44.2

`Release 1.44.1`_ (2016-02-18)
------------------------------

- Fixed a non-fatal but obviously wrong log format error in ``prompt_for_choice()``.
- Added Python 3.5 to supported versions on Travis CI and in the documentation.

.. _Release 1.44.1: https://github.com/xolox/python-humanfriendly/compare/1.44...1.44.1

`Release 1.44`_ (2016-02-17)
----------------------------

Added the ``humanfriendly.sphinx`` module with automagic usage message
reformatting and a bit of code that I'd been copying and pasting between
``docs/conf.py`` scripts for years to include magic methods, etc in
Sphinx generated documentation.

.. _Release 1.44: https://github.com/xolox/python-humanfriendly/compare/1.43.1...1.44

`Release 1.43.1`_ (2016-01-19)
------------------------------

Bug fix for Python 2.6 compatibility in ``setup.py`` script.

.. _Release 1.43.1: https://github.com/xolox/python-humanfriendly/compare/1.43...1.43.1

`Release 1.43`_ (2016-01-19)
----------------------------

Replaced ``import_module()`` with a conditional dependency on ``importlib``.

.. _Release 1.43: https://github.com/xolox/python-humanfriendly/compare/1.42...1.43

`Release 1.42`_ (2015-10-23)
----------------------------

Added proper tests for ANSI escape sequence support.

.. _Release 1.42: https://github.com/xolox/python-humanfriendly/compare/1.41...1.42

`Release 1.41`_ (2015-10-22)
----------------------------

- Moved hard coded ANSI text style codes to a module level ``ANSI_TEXT_STYLES`` dictionary.
- Improved the related error reporting based on the new dictionary.

.. _Release 1.41: https://github.com/xolox/python-humanfriendly/compare/1.40...1.41

`Release 1.40`_ (2015-10-22)
----------------------------

Added support for custom delimiters in ``humanfriendly.text.split()``.

.. _Release 1.40: https://github.com/xolox/python-humanfriendly/compare/1.39...1.40

`Release 1.39`_ (2015-10-22)
----------------------------

Added the ``humanfriendly.compat`` module to group Python 2 / 3 compatibility logic.

.. _Release 1.39: https://github.com/xolox/python-humanfriendly/compare/1.38...1.39

`Release 1.38`_ (2015-10-22)
----------------------------

- Added the ``prompt_for_confirmation()`` function to render (y/n) prompts.
- Improved the prompt rendered by ``prompt_for_choice()``.
- Extracted supporting prompt functionality to separate functions.

.. _Release 1.38: https://github.com/xolox/python-humanfriendly/compare/1.37...1.38

`Release 1.37`_ (2015-10-22)
----------------------------

- Added support for wrapping ANSI escape sequences in "readline hints".
- Work around incompatibility between ``flake8-pep257==1.0.3`` and ``pep257==0.7.0``.

.. _Release 1.37: https://github.com/xolox/python-humanfriendly/compare/1.36...1.37

`Release 1.36`_ (2015-10-21)
----------------------------

Added ``message()`` and ``warning()`` functions to write informational and
warning messages to the terminal (on the standard error stream).

.. _Release 1.36: https://github.com/xolox/python-humanfriendly/compare/1.35...1.36

`Release 1.35`_ (2015-09-10)
----------------------------

Implemented the feature request in issue #6: Support for milleseconds in
timespan parsing/formatting. Technically speaking this breaks backwards
compatibility but only by dropping a nasty (not documented) implementation
detail. Quoting from the old code::

  # All of the first letters of the time units are unique, so
  # although this check is not very strict I believe it to be
  # sufficient.

That no longer worked with [m]illiseconds versus [m]inutes as was
also evident from the feature request / bug report on GitHub.

.. _Release 1.35: https://github.com/xolox/python-humanfriendly/compare/1.34...1.35

`Release 1.34`_ (2015-08-06)
----------------------------

Implemented and added checks to enforce PEP-8 and PEP-257 compliance.

.. _Release 1.34: https://github.com/xolox/python-humanfriendly/compare/1.33...1.34

`Release 1.33`_ (2015-07-27)
----------------------------

Added ``format_length()`` and `parse_length()`` functions via `pull request #5`_.

.. _Release 1.33: https://github.com/xolox/python-humanfriendly/compare/1.32...1.33
.. _pull request #5: https://github.com/xolox/python-humanfriendly/pull/5

`Release 1.32`_ (2015-07-19)
----------------------------

Added the ``humanfriendly.text.split()`` function.

.. _Release 1.32: https://github.com/xolox/python-humanfriendly/compare/1.31...1.32

`Release 1.31`_ (2015-06-28)
----------------------------

Added support for rendering of usage messages to reStructuredText.

.. _Release 1.31: https://github.com/xolox/python-humanfriendly/compare/1.30...1.31

`Release 1.30`_ (2015-06-28)
----------------------------

Started moving functions to separate modules.

.. _Release 1.30: https://github.com/xolox/python-humanfriendly/compare/1.29...1.30

`Release 1.29`_ (2015-06-24)
----------------------------

Added the ``parse_timespan()`` function.

.. _Release 1.29: https://github.com/xolox/python-humanfriendly/compare/1.28...1.29

`Release 1.28`_ (2015-06-24)
----------------------------

Extracted the "new" ``tokenize()`` function from the existing ``parse_size()`` function.

.. _Release 1.28: https://github.com/xolox/python-humanfriendly/compare/1.27...1.28

`Release 1.27`_ (2015-06-03)
----------------------------

Changed table formatting to right-align table columns with numeric data (and
pimped the documentation).

.. _Release 1.27: https://github.com/xolox/python-humanfriendly/compare/1.26...1.27

`Release 1.26`_ (2015-06-02)
----------------------------

Make table formatting 'smart' by having it automatically handle overflow of
columns by switching to a different more verbose vertical table layout.

.. _Release 1.26: https://github.com/xolox/python-humanfriendly/compare/1.25.1...1.26

`Release 1.25.1`_ (2015-06-02)
------------------------------

- Bug fix for a somewhat obscure ``UnicodeDecodeError`` in ``setup.py`` on Python 3.
- Travis CI now also runs the test suite on PyPy.
- Documented PyPy compatibility.

.. _Release 1.25.1: https://github.com/xolox/python-humanfriendly/compare/1.25...1.25.1

`Release 1.25`_ (2015-05-27)
----------------------------

Added the ``humanfriendly.terminal.usage()`` function for nice rendering of
usage messages on interactive terminals (try ``humanfriendly --help`` to see it
in action).

.. _Release 1.25: https://github.com/xolox/python-humanfriendly/compare/1.24...1.25

`Release 1.24`_ (2015-05-27)
----------------------------

Added the ``humanfriendly.terminal`` module with support for ANSI escape
sequences, detecting interactive terinals, finding the terminal size, etc.

.. _Release 1.24: https://github.com/xolox/python-humanfriendly/compare/1.23.1...1.24

`Release 1.23.1`_ (2015-05-26)
------------------------------

Bug fix for Python 3 compatibility in ``format_table()``.

.. _Release 1.23.1: https://github.com/xolox/python-humanfriendly/compare/1.23...1.23.1

`Release 1.23`_ (2015-05-26)
----------------------------

Added ``format_table()`` function to format tabular data in simple textual tables.

.. _Release 1.23: https://github.com/xolox/python-humanfriendly/compare/1.22...1.23

`Release 1.22`_ (2015-05-26)
----------------------------

Added additional string formatting functions ``compact()``, ``dedent()``,
``format()``, ``is_empty_line()`` and ``trim_empty_lines()``.

.. _Release 1.22: https://github.com/xolox/python-humanfriendly/compare/1.21...1.22

`Release 1.21`_ (2015-05-25)
----------------------------

Added support for formatting numbers with thousands separators.

.. _Release 1.21: https://github.com/xolox/python-humanfriendly/compare/1.20...1.21

`Release 1.20`_ (2015-05-25)
----------------------------

- Added a simple command line interface.
- Added trove classifiers to ``setup.py``.

.. _Release 1.20: https://github.com/xolox/python-humanfriendly/compare/1.19...1.20

`Release 1.19`_ (2015-05-23)
----------------------------

Made it possible to use spinners as context managers.

.. _Release 1.19: https://github.com/xolox/python-humanfriendly/compare/1.18...1.19

`Release 1.18`_ (2015-05-23)
----------------------------

Added a ``Spinner.sleep()`` method.

.. _Release 1.18: https://github.com/xolox/python-humanfriendly/compare/1.17...1.18

`Release 1.17`_ (2015-05-23)
----------------------------

- Improved interaction between spinner & verbose log outputs: The spinner until
  now didn't end each string of output with a carriage return because then the
  text cursor would jump to the start of the screen line and disturb the
  spinner, however verbose log output and the spinner don't interact well
  because of this, so I've decided to hide the text cursor while the spinner is
  active.
- Added another example to the documentation of ``parse_date()``.

.. _Release 1.17: https://github.com/xolox/python-humanfriendly/compare/1.16...1.17

`Release 1.16`_ (2015-03-29)
----------------------------

- Change spinners to use the 'Erase in Line' ANSI escape code to properly clear screen lines.
- Improve performance of Travis CI and increase multiprocessing test coverage.

.. _Release 1.16: https://github.com/xolox/python-humanfriendly/compare/1.15...1.16

`Release 1.15`_ (2015-03-17)
----------------------------

- Added support for ``AutomaticSpinner`` that animates without requiring ``step()`` calls.
- Changed the Python package layout so that all ``*.py`` files are in one directory.
- Added tests for ``parse_path()`` and ``Timer.rounded``.

.. _Release 1.15: https://github.com/xolox/python-humanfriendly/compare/1.14...1.15

`Release 1.14`_ (2014-11-22)
----------------------------

- Changed ``coerce_boolean()`` to coerce empty strings to ``False``.
- Added ``parse_path()`` function (a simple combination of standard library functions that I've repeated numerous times).

.. _Release 1.14: https://github.com/xolox/python-humanfriendly/compare/1.13...1.14

`Release 1.13`_ (2014-11-16)
----------------------------

Added support for spinners with an embedded timer.

.. _Release 1.13: https://github.com/xolox/python-humanfriendly/compare/1.12...1.13

`Release 1.12`_ (2014-11-16)
----------------------------

Added support for rounded timestamps.

.. _Release 1.12: https://github.com/xolox/python-humanfriendly/compare/1.11...1.12

`Release 1.11`_ (2014-11-15)
----------------------------

Added ``coerce_boolean()`` function.

.. _Release 1.11: https://github.com/xolox/python-humanfriendly/compare/1.10...1.11

`Release 1.10`_ (2014-11-15)
----------------------------

Improved ``pluralize()`` by making it handle the simple case of pluralizing by adding 's'.

.. _Release 1.10: https://github.com/xolox/python-humanfriendly/compare/1.9.6...1.10

`Release 1.9.6`_ (2014-09-14)
-----------------------------

Improved the documentation by adding a few docstring examples via pull request `#3`_.

.. _Release 1.9.6: https://github.com/xolox/python-humanfriendly/compare/1.9.5...1.9.6
.. _#3: https://github.com/xolox/python-humanfriendly/pull/3

`Release 1.9.5`_ (2014-06-29)
-----------------------------

Improved the test suite by making the timing related tests less sensitive to
slow test execution. See
https://travis-ci.org/xolox/python-humanfriendly/jobs/28706938 but the same
thing can happen anywhere. When looked at from that perspective the fix I'm
committing here really isn't a fix, but I suspect it will be fine :-).

.. _Release 1.9.5: https://github.com/xolox/python-humanfriendly/compare/1.9.4...1.9.5

`Release 1.9.4`_ (2014-06-29)
-----------------------------

- Fixed Python 3 compatibility (``input()`` versus ``raw_input()``). See https://travis-ci.org/xolox/python-humanfriendly/jobs/28700750.
- Removed a ``print()`` in the test suite, left over from debugging.

.. _Release 1.9.4: https://github.com/xolox/python-humanfriendly/compare/1.9.3...1.9.4

`Release 1.9.3`_ (2014-06-29)
-----------------------------

- Automatically disable ``Spinner`` when ``stream.isatty()`` returns ``False``.
- Improve the makefile by adding ``install`` and ``coverage`` targets.
- Remove the makefile generated by Sphinx (all we need from it is one command).
- Add unit tests for ``prompt_for_choice()`` bringing coverage back up to 95%.

.. _Release 1.9.3: https://github.com/xolox/python-humanfriendly/compare/1.9.2...1.9.3

`Release 1.9.2`_ (2014-06-29)
-----------------------------

Added support for 'B' bytes unit to ``parse_size()`` via `pull request #2`_.

.. _Release 1.9.2: https://github.com/xolox/python-humanfriendly/compare/1.9.1...1.9.2
.. _pull request #2: https://github.com/xolox/python-humanfriendly/pull/2

`Release 1.9.1`_ (2014-06-23)
-----------------------------

Improved the ``prompt_for_choice()`` function by clearly presenting the default
choice (if any).

.. _Release 1.9.1: https://github.com/xolox/python-humanfriendly/compare/1.9...1.9.1

`Release 1.9`_ (2014-06-23)
---------------------------

Added the ``prompt_for_choice()`` function.

.. _Release 1.9: https://github.com/xolox/python-humanfriendly/compare/1.8.6...1.9

`Release 1.8.6`_ (2014-06-08)
-----------------------------

Enable ``Spinner`` to show progress counter (percentage).

.. _Release 1.8.6: https://github.com/xolox/python-humanfriendly/compare/1.8.5...1.8.6

`Release 1.8.5`_ (2014-06-08)
-----------------------------

Make ``Timer`` objects "resumable".

.. _Release 1.8.5: https://github.com/xolox/python-humanfriendly/compare/1.8.4...1.8.5

`Release 1.8.4`_ (2014-06-07)
-----------------------------

Make the ``Spinner(label=...)`` argument optional.

.. _Release 1.8.4: https://github.com/xolox/python-humanfriendly/compare/1.8.3...1.8.4

`Release 1.8.3`_ (2014-06-07)
-----------------------------

Make it possible to override the label for individual steps of spinners.

.. _Release 1.8.3: https://github.com/xolox/python-humanfriendly/compare/1.8.2...1.8.3

`Release 1.8.2`_ (2014-06-01)
-----------------------------

Automatically rate limit ``Spinner`` instances.

.. _Release 1.8.2: https://github.com/xolox/python-humanfriendly/compare/1.8.1...1.8.2

`Release 1.8.1`_ (2014-05-11)
-----------------------------

- Improve Python 3 compatibility: Make sure sequences passed to ``concatenate()`` are lists.
- Submit test coverage from Travis CI to Coveralls.io.
- Increase test coverage of ``concatenate()``, ``Spinner()`` and ``Timer()``.
- Use ``assertRaises()`` instead of ``try``, ``except`` and ``isinstance()`` in test suite.

.. _Release 1.8.1: https://github.com/xolox/python-humanfriendly/compare/1.8...1.8.1

`Release 1.8`_ (2014-05-10)
---------------------------

- Added support for Python 3 thanks to a pull request.
- Document the supported Python versions (2.6, 2.7 and 3.4).
- Started using Travis CI to automatically run the test suite.

.. _Release 1.8: https://github.com/xolox/python-humanfriendly/compare/1.7.1...1.8

`Release 1.7.1`_ (2013-09-22)
-----------------------------

Bug fix for ``concatenate()`` when given only one item.

.. _Release 1.7.1: https://github.com/xolox/python-humanfriendly/compare/1.7...1.7.1

`Release 1.7`_ (2013-09-22)
---------------------------

Added functions ``concatenate()`` and ``pluralize()``, both originally
developed in private scripts.

.. _Release 1.7: https://github.com/xolox/python-humanfriendly/compare/1.6.1...1.7

`Release 1.6.1`_ (2013-09-22)
-----------------------------

Bug fix: Don't raise an error in ``format_path()`` if $HOME isn't set.

.. _Release 1.6.1: https://github.com/xolox/python-humanfriendly/compare/1.6...1.6.1

`Release 1.6`_ (2013-08-12)
---------------------------

Added a ``Spinner`` class that I originally developed for `pip-accel
`_.

.. _Release 1.6: https://github.com/xolox/python-humanfriendly/compare/1.5...1.6

`Release 1.5`_ (2013-07-07)
---------------------------

Added a ``Timer`` class to easily keep track of long running operations.

.. _Release 1.5: https://github.com/xolox/python-humanfriendly/compare/1.4.3...1.5

`Release 1.4.3`_ (2013-07-06)
-----------------------------

Fixed various edge cases in ``format_path()``, making it more robust.

.. _Release 1.4.3: https://github.com/xolox/python-humanfriendly/compare/1.4.2...1.4.3

`Release 1.4.2`_ (2013-06-27)
-----------------------------

Improved the project description in ``setup.py`` and added a link to online
documentation on PyPI.

.. _Release 1.4.2: https://github.com/xolox/python-humanfriendly/compare/1.4.1...1.4.2

`Release 1.4.1`_ (2013-06-27)
-----------------------------

Renamed the package from ``human-friendly`` to ``humanfriendly``.

.. _Release 1.4.1: https://github.com/xolox/python-humanfriendly/compare/1.4...1.4.1

`Release 1.4`_ (2013-06-17)
---------------------------

Added the ``parse_date()`` function.

.. _Release 1.4: https://github.com/xolox/python-humanfriendly/compare/1.3.1...1.4

`Release 1.3.1`_ (2013-06-17)
-----------------------------

- Improved the documentation by adding lots of examples.
- Renamed the arguments to the ``format_size()`` and ``format_timespan()`` functions.

.. _Release 1.3.1: https://github.com/xolox/python-humanfriendly/compare/1.3...1.3.1

`Release 1.3`_ (2013-06-17)
---------------------------

Added the ``format_timespan()`` function.

.. _Release 1.3: https://github.com/xolox/python-humanfriendly/compare/1.2...1.3

`Release 1.2`_ (2013-06-17)
---------------------------

Started using Sphinx to generate API documentation from docstrings.

.. _Release 1.2: https://github.com/xolox/python-humanfriendly/compare/1.1...1.2

`Release 1.1`_ (2013-06-17)
---------------------------

Added the ``format_path()`` function.

.. _Release 1.1: https://github.com/xolox/python-humanfriendly/compare/1.0...1.1

`Release 1.0`_ (2013-06-17)
---------------------------

The initial commit of the project, created by gathering functions from various
personal scripts that I wrote over the past years.

.. _Release 1.0: https://github.com/xolox/python-humanfriendly/tree/1.0
humanfriendly-4.18/constraints.txt0000644000175000017500000000302213433604150017677 0ustar  peterpeter00000000000000# This is a pip constraints file that is used to preserve Python 2.6
# compatibility (on Travis CI). Why I'm still doing that in 2018 is
# a good question, maybe simply to prove that I can :-P.

# flake8 3.0.0 drops explicit support for Python 2.6:
# http://flake8.pycqa.org/en/latest/release-notes/3.0.0.html
flake8 < 3.0.0 ; python_version < '2.7'

# flake8-docstrings 1.0.0 switches from pep257 to pydocstyle and I haven't been
# able to find a combination of versions of flake8-docstrings and pydocstyle
# that actually works on Python 2.6. Here's the changelog:
# https://gitlab.com/pycqa/flake8-docstrings/blob/master/HISTORY.rst
flake8-docstrings < 1.0.0 ; python_version < '2.7'

# pyflakes 2.0.0 drops Python 2.6 compatibility:
# https://github.com/PyCQA/pyflakes/blob/master/NEWS.txt
pyflakes < 2.0.0 ; python_version < '2.7'

# pytest 3.3 drops Python 2.6 compatibility:
# https://docs.pytest.org/en/latest/changelog.html#pytest-3-3-0-2017-11-23
pytest < 3.3 ; python_version < '2.7'

# pytest-cov 2.6.0 drops Python 3.4 compatibility:
# https://pytest-cov.readthedocs.io/en/latest/changelog.html
pytest-cov < 2.6.0 ; python_version < '3.5'

# attrs 16.0.0 (used by pytest) drops Python 2.6 compatibility:
# http://www.attrs.org/en/stable/changelog.html
attrs < 16.0.0 ; python_version < '2.7'

# pycparser < 2.19 drops Python 2.6 compatibility:
# https://github.com/eliben/pycparser/blob/master/CHANGES
pycparser < 2.19 ; python_version < '2.7'

# idna 2.8 drops Python 2.6 compatibility (not documented).
idna < 2.8 ; python_version < '2.7'
humanfriendly-4.18/setup.py0000755000175000017500000001236413362535475016333 0ustar  peterpeter00000000000000#!/usr/bin/env python

# Setup script for the `humanfriendly' package.
#
# Author: Peter Odding 
# Last Change: October 20, 2018
# URL: https://humanfriendly.readthedocs.io

"""
Setup script for the `humanfriendly` package.

**python setup.py install**
  Install from the working directory into the current Python environment.

**python setup.py sdist**
  Build a source distribution archive.

**python setup.py bdist_wheel**
  Build a wheel distribution archive.
"""

# Standard library modules.
import codecs
import os
import re
import sys

# De-facto standard solution for Python packaging.
from setuptools import find_packages, setup


def get_contents(*args):
    """Get the contents of a file relative to the source distribution directory."""
    with codecs.open(get_absolute_path(*args), 'r', 'UTF-8') as handle:
        return handle.read()


def get_version(*args):
    """Extract the version number from a Python module."""
    contents = get_contents(*args)
    metadata = dict(re.findall('__([a-z]+)__ = [\'"]([^\'"]+)', contents))
    return metadata['version']


def get_install_requires():
    """Get the conditional dependencies for source distributions."""
    install_requires = []
    if 'bdist_wheel' not in sys.argv:
        if sys.version_info[:2] <= (2, 6) or sys.version_info[:2] == (3, 0):
            install_requires.extend(('importlib', 'unittest2'))
        if sys.version_info[:2] < (3, 3):
            install_requires.append('monotonic')
        if sys.platform == 'win32':
            install_requires.append('pyreadline')
    return sorted(install_requires)


def get_extras_require():
    """Get the conditional dependencies for wheel distributions."""
    extras_require = {}
    if have_environment_marker_support():
        # Conditional `importlib' and `unittest2' dependencies.
        expression = ':%s' % ' or '.join([
            'python_version == "2.6"',
            'python_version == "3.0"',
        ])
        extras_require[expression] = ['importlib', 'unittest2']
        # Conditional `monotonic' dependency.
        expression = ':%s' % ' or '.join([
            'python_version == "2.6"',
            'python_version == "2.7"',
            'python_version == "3.0"',
            'python_version == "3.1"',
            'python_version == "3.2"',
        ])
        extras_require[expression] = ['monotonic']
        # Conditional `pyreadline' dependency.
        expression = ':sys_platform == "win32"'
        extras_require[expression] = 'pyreadline'
    return extras_require


def get_absolute_path(*args):
    """Transform relative pathnames into absolute pathnames."""
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), *args)


def have_environment_marker_support():
    """
    Check whether setuptools has support for PEP-426 environment marker support.

    Based on the ``setup.py`` script of the ``pytest`` package:
    https://bitbucket.org/pytest-dev/pytest/src/default/setup.py
    """
    try:
        from pkg_resources import parse_version
        from setuptools import __version__
        return parse_version(__version__) >= parse_version('0.7.2')
    except Exception:
        return False


setup(
    name='humanfriendly',
    version=get_version('humanfriendly', '__init__.py'),
    description="Human friendly output for text interfaces using Python",
    long_description=get_contents('README.rst'),
    url='https://humanfriendly.readthedocs.io',
    author="Peter Odding",
    author_email='peter@peterodding.com',
    license='MIT',
    packages=find_packages(),
    entry_points=dict(console_scripts=[
        'humanfriendly = humanfriendly.cli:main',
    ]),
    install_requires=get_install_requires(),
    extras_require=get_extras_require(),
    test_suite='humanfriendly.tests',
    tests_require=[
        'capturer >= 2.1',
        'coloredlogs >= 2.0',
    ],
    classifiers=[
        'Development Status :: 6 - Mature',
        'Environment :: Console',
        'Framework :: Sphinx :: Extension',
        'Intended Audience :: Developers',
        'Intended Audience :: System Administrators',
        'License :: OSI Approved :: MIT License',
        'Natural Language :: English',
        'Programming Language :: Python',
        'Programming Language :: Python :: 2',
        'Programming Language :: Python :: 2.6',
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.4',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: Implementation :: CPython',
        'Programming Language :: Python :: Implementation :: PyPy',
        'Topic :: Communications',
        'Topic :: Scientific/Engineering :: Human Machine Interfaces',
        'Topic :: Software Development',
        'Topic :: Software Development :: Libraries :: Python Modules',
        'Topic :: Software Development :: User Interfaces',
        'Topic :: System :: Shells',
        'Topic :: System :: System Shells',
        'Topic :: System :: Systems Administration',
        'Topic :: Terminals',
        'Topic :: Text Processing :: General',
        'Topic :: Text Processing :: Linguistic',
        'Topic :: Utilities',
    ])
humanfriendly-4.18/LICENSE.txt0000664000175000017500000000204013223414325016413 0ustar  peterpeter00000000000000Copyright (c) 2018 Peter Odding

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.