# Imports
from bs4 import BeautifulSoup
import csv
import io
import logging
import os
import re
from shutil import copy2
import six
from .constants import BASE10, BASE62, BOOLEAN_VALUES, FALSE_VALUES, TRUE_VALUES
log = logging.getLogger(__name__)
# Exports
__all__ = (
"append_file",
"average",
"base_convert",
"camelcase_to_underscore",
"copy_file",
"copy_tree",
"indent",
"is_bool",
"is_integer",
"is_string",
"percentage",
"read_csv",
"read_file",
"smart_cast",
"strip_html_tags",
"to_bool",
"truncate",
"underscore_to_camelcase",
"underscore_to_title_case",
"write_file",
"File",
)
# Functions
[docs]def append_file(path, content):
"""Append to a file.
:param path: The path to the file.
:type path: str
:param content: The content to be appended the file.
:type content: str
.. code-block:: python
from superdjango.utils import append_file
append_file("path/to/readme.txt", "This is a test.")
"""
with io.open(path, "a", encoding="utf-8") as f:
f.write(content)
f.close()
[docs]def average(values):
"""Calculate the average of a given number of values.
:param values: The values to be averaged.
:type values: list | tuple
:rtype: float
Ever get tired of creating a try/except for zero division? I do.
.. code-block:: python
from superdjango.utils import average
values = [1, 2, 3, 4, 5]
print(average(values))
"""
try:
return float(sum(values) / len(values))
except ZeroDivisionError:
return 0.0
[docs]def base_convert(number, from_digits=BASE10, to_digits=BASE62):
"""Convert a number between two bases of arbitrary digits.
:param number: The number to be converted.
:type number: int
:param from_digits: The digits to use as the source of the conversion. ``number`` is included in these digits.
:type from_digits: str
:param to_digits: The digits to which the number will be converted.
:type to_digits: str
:rtype: str
"""
if str(number)[0] == '-':
number = str(number)[1:]
negative = True
else:
negative = False
x = 0
for digit in str(number):
x = x * len(from_digits) + from_digits.index(digit)
if x == 0:
result = to_digits[0]
else:
result = ""
while x > 0:
digit = x % len(to_digits)
result = to_digits[digit] + result
x = int(x / len(to_digits))
if negative:
result = "-" + result
return result
[docs]def camelcase_to_underscore(string):
"""Convert a given string from ``CamelCase`` to ``camel_case``.
:param string: The string to be converted.
:type string: str
:rtype: str
"""
# http://djangosnippets.org/snippets/585/
return re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', '_\\1', string).lower().strip('_')
[docs]def copy_file(from_path, to_path, make_directories=False):
"""Copy a file from one location to another.
:param from_path: The source path.
:type from_path: str || unicode
:param to_path: The destination path.
:type to_path: str || unicode
:param make_directories: Create directories as needed along the ``to_path``.
:type make_directories: bool
:rtype: tuple(bool, str)
:returns: Success or failure and a message if failure.
.. code-block:: python
from superdjango.utils import copy_file
copy_file("readme-template.txt", "path/to/project/readme.txt")
"""
if make_directories:
base_path = os.path.dirname(to_path)
if not os.path.exists(base_path):
os.makedirs(base_path)
try:
copy2(from_path, to_path)
return True, None
except IOError as e:
return False, str(e)
[docs]def copy_tree(from_path, to_path):
"""Recursively copy a source directory to a given destination.
:param from_path: The source directory.
:type from_path: str
:param to_path: The destination directory. This must already exist.
:type to_path: str
:rtype: bool
:returns: ``True`` if successful.
.. note::
Errors are logged using the Python log.
.. code-block:: python
from superdjango.utils import copy_tree
success = copy_tree("from/path", "to/path")
print(success)
"""
# Deal with absolutes and user expansion.
source = os.path.abspath(os.path.expanduser(from_path))
destination = os.path.abspath(os.path.expanduser(to_path))
if not os.path.exists(destination):
log.error("Destination does not exist: %s" % destination)
return False
# Iterate through the source.
success = True
for root, dirs, files in os.walk(source):
directory_path = os.path.join(destination, os.path.relpath(root, source))
if not os.path.exists(directory_path):
os.mkdir(directory_path)
for f in files:
source_file = os.path.join(root, f)
file_path = os.path.join(directory_path, f)
try:
copy2(source_file, file_path)
except IOError as e:
success = False
log.warning("Could not copy %s: %s" % (source_file, e))
return success
[docs]def indent(text, amount=4):
"""Indent a string.
:param text: The text to be indented.
:type text: str
:param amount: The number of spaces to use for indentation.
:type amount: int
:rtype: str
.. code-block:: python
from superdjango.utils import indent
text = "This text will be indented."
print(indent(text))
"""
prefix = " " * amount
return prefix + text.replace('\n', '\n' + prefix)
[docs]def is_bool(value, test_values=BOOLEAN_VALUES):
"""Determine if the given value is a boolean at run time.
:param value: The value to be checked.
:param test_values: The possible values that could be True or False.
:type test_values: list | tuple
:rtype: bool
.. code-block:: python
from superdjango.utils import is_bool
print(is_bool("yes"))
print(is_bool(True))
print(is_bool("No"))
print(is_bool(False))
.. note::
By default, a liberal number of values are used to test. If you *just* want ``True`` or ``False``, simply pass
``(True, False)`` as ``test_values``.
"""
return value in test_values
[docs]def is_integer(value, cast=False):
"""Indicates whether the given value is an integer. Saves a little typing.
:param value: The value to be checked.
:param cast: Indicates whether the value (when given as a string) should be cast to an integer.
:type cast: bool
:rtype: bool
.. code-block:: python
from superdjango.utils import is_integer
print(is_integer(17))
print(is_integer(17.5))
print(is_integer("17"))
print(is_integer("17", cast=True))
"""
if isinstance(value, int):
return True
if isinstance(value, (float, str)) and cast:
try:
int(value)
except ValueError:
return False
else:
return True
return False
[docs]def is_string(value):
"""Indicates whether the given value is a string. Saves a little typing.
:param value: The value to be checked.
:rtype: bool
.. code-block:: python
from superdjango.utils import is_string
print(is_string("testing"))
print(is_string("17"))
print(is_string(17))
"""
return isinstance(value, six.string_types)
# def match_all(*conditions, lazy=False):
# """Evaluate all provided conditions.
#
# :param conditions: A list of boolean conditions.
#
# :param lazy: Indicates that each condition may be evaluated separately, but all must be ``True``. This is useful
# when one or more conditions are dependent upon previous conditions.
#
# :rtype: bool
# :returns: ``True`` if all conditions are ``True``.
#
# """
# if not lazy:
# return all(conditions)
#
# results = list()
# for c in conditions:
# if c == True:
# results.append(True)
# else:
# results.append(False)
#
# return all(results)
#
#
# def match_any(*conditions, lazy=False):
# """Evaluate any provided conditions.
#
# :param conditions: A list of boolean conditions.
#
# :param lazy: Indicates that each condition may be evaluated separately, but at least one must be ``True``. This is
# useful when one or more conditions are dependent upon previous conditions.
#
# :rtype: bool
# :returns: ``True`` if any conditions are ``True``.
#
# """
# if not lazy:
# return any(conditions)
#
# results = list()
# for c in conditions:
# if c == True:
# results.append(True)
# else:
# results.append(False)
#
# return any(results)
[docs]def percentage(portion, total):
"""Calculate the percentage that a portion makes up of a total.
:param portion: The portion of the total to be calculated as a percentage.
:type portion: float | int
:param total: The total amount.
:type total: float | int
:rtype: float
.. code-block:: python
from superdjango.utils import percentage
p = percentage(50, 100)
print(p + "%")
"""
try:
return 100.0 * portion / total
except (TypeError, ZeroDivisionError):
return 0.0
[docs]def read_csv(path, encoding="utf-8", first_row_field_names=False):
"""Read the contents of a CSV file.
:param path: The path to the file.
:type path: str
:param encoding: The encoding of the file.
:type encoding: str
:param first_row_field_names: Indicates the first row contains the field names. In this case the returned rows will
be a dictionary rather than a list.
:type first_row_field_names: bool
:rtype: list[list] || list[dict]
.. code-block:: text
menu,identifier,sort_order,text,url
main,product,10,Product,/product/
main,solutions,20,Solutions,/solutions/
main,resources,30,Resources,/resources/
main,support,40,Support,https://support.example.com
main,about,50,About,/about/
main,contact,60,Contact,/contact/
.. code-block:: python
from superdjango.utils import read_csv
rows = read_csv("path/to/menus.csv", first_row_fields_names=True)
for r in rows:
print("%s: %s" % (row['identifier'], row['url']
"""
with io.open(path, "r", encoding=encoding) as f:
if first_row_field_names:
reader = csv.DictReader(f)
else:
reader = csv.reader(f)
rows = list()
for row in reader:
rows.append(row)
f.close()
return rows
[docs]def read_file(path):
"""Read a file and return its contents.
:param path: The path to the file.
:type path: str || unicode
:rtype: str
.. code-block:: python
from superdjango.utils import read_file
output = read_file("path/to/readme.txt")
print(output)
"""
with io.open(path, "r", encoding="utf-8") as f:
output = f.read()
f.close()
return output
[docs]def smart_cast(value):
"""Intelligently cast the given value to a Python data type.
:param value: The value to be cast.
:type value: str
"""
# Handle integers first because is_bool() may interpret 0s and 1s ad booleans.
if is_integer(value, cast=True):
return int(value)
elif is_bool(value):
return to_bool(value)
else:
return value
[docs]def to_bool(value, false_values=FALSE_VALUES, true_values=TRUE_VALUES):
"""Convert the given value to it's boolean equivalent.
:param value: The value to be converted.
:param false_values: The possible values that could be False.
:type false_values: list | tuple
:param true_values: The possible values that could be True.
:type true_values: list | tuple
:rtype: bool
:raises: ``ValueError`` if the value could not be converted.
.. code-block:: python
from superdjango.utils import to_bool
print(to_bool("yes"))
print(to_bool(1))
print(to_bool("no"))
print(to_bool(0))
"""
if value in true_values:
return True
if value in false_values:
return False
raise ValueError('"%s" cannot be converted to True or False.')
[docs]def truncate(string, continuation="...", limit=30):
"""Get a truncated version of a string if if over the limit.
:param string: The string to be truncated.
:type string: str || None
:param limit: The maximum number of characters.
:type limit: int
:param continuation: The string to add to the truncated title.
:type continuation: str || None
:rtype: str
.. code-block:: python
from superdjango.utils import truncate
title = "This Title is Too Long to Be Displayed As Is"
print(truncate(title))
"""
# Make it safe to submit the string as None.
if string is None:
return ""
# There's nothing to do if the string is not over the limit.
if len(string) <= limit:
return string
# Adjust the limit according to the string length, otherwise we'll still be over.
if continuation:
limit -= len(continuation)
# Return the altered title.
if continuation:
return string[:limit] + continuation
else:
return string[:limit]
[docs]def underscore_to_camelcase(string):
"""Convert a string with underscore separations to CamelCase.
:param string: The string to be converted.
:type string: str
:rtype: str
"""
return string.replace("_", " ").title().replace(" ", "")
[docs]def underscore_to_title_case(string):
"""Convert a string to title case.
:param string: The string to be converted.
:type string: str
:rtype: str
"""
return string.replace("_", " ").title()
[docs]def write_file(path, content="", make_directories=False):
"""Write a file.
:param path: The path to the file.
:type path: str || unicode
:param content: The content of the file. An empty string is effectively the same as a "touch".
:type content: str || unicode
:param make_directories: Create directories as needed along the file path.
:type make_directories: bool
.. code-block:: python
from superdjango.utils import write_file
write_file("path/to/readme.txt", "This is a test.")
"""
if make_directories:
base_path = os.path.dirname(path)
if not os.path.exists(base_path):
os.makedirs(base_path)
with io.open(path, "w", encoding="utf-8") as f:
f.write(content)
f.close()
# Classes
[docs]class File(object):
"""A simple helper class for working with file names.
For more robust handling of paths, see `pathlib`_.
.. _pathlib: https://docs.python.org/3/library/pathlib.html
"""
[docs] def __init__(self, path):
"""Initialize the file instance.
:param path: The path to the file.
:type path: str
"""
self.basename = os.path.basename(path)
self.directory = os.path.dirname(path)
self.extension = os.path.splitext(path)[-1]
self.name = os.path.basename(os.path.splitext(path)[0])
self.path = path
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.basename)
@property
def exists(self):
"""Indicates the file exists.
:rtype: bool
"""
return os.path.exists(self.path)