X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=string_utils.py;h=244c450b5ac4b1ec89d8388de6d32eb592b40225;hb=4c8cd8d958afa46eafceda7a7295e31bfd884a8c;hp=a6a2da3155fcf312e9a9bc21ecd94f26141b4fcb;hpb=9484900f080e16f118806fe54973e69d36b043e8;p=python_utils.git diff --git a/string_utils.py b/string_utils.py index a6a2da3..244c450 100644 --- a/string_utils.py +++ b/string_utils.py @@ -11,9 +11,19 @@ import numbers import random import re import string -from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, +) import unicodedata from uuid import uuid4 +import warnings import list_utils @@ -31,7 +41,7 @@ URLS_RAW_STRING = ( r"([a-z-]+://)" # scheme r"([a-z_\d-]+:[a-z_\d-]+@)?" # user:password r"(www\.)?" # www. - r"((? bool: False """ return ( - (is_number(in_str) and "." not in in_str) or - is_hexidecimal_integer_number(in_str) or - is_octal_integer_number(in_str) or - is_binary_integer_number(in_str) + (is_number(in_str) and "." not in in_str) + or is_hexidecimal_integer_number(in_str) + or is_octal_integer_number(in_str) + or is_binary_integer_number(in_str) ) @@ -381,10 +391,7 @@ def strip_escape_sequences(in_str: str) -> str: def add_thousands_separator( - in_str: str, - *, - separator_char = ',', - places = 3 + in_str: str, *, separator_char=',', places=3 ) -> str: """ Add thousands separator to a numeric string. Also handles numbers. @@ -405,20 +412,21 @@ def add_thousands_separator( in_str = f'{in_str}' if is_number(in_str): return _add_thousands_separator( - in_str, - separator_char = separator_char, - places = places + in_str, separator_char=separator_char, places=places ) raise ValueError(in_str) -def _add_thousands_separator(in_str: str, *, separator_char = ',', places = 3) -> str: +def _add_thousands_separator( + in_str: str, *, separator_char=',', places=3 +) -> str: decimal_part = "" if '.' in in_str: (in_str, decimal_part) = in_str.split('.') tmp = [iter(in_str[::-1])] * places ret = separator_char.join( - "".join(x) for x in zip_longest(*tmp, fillvalue=""))[::-1] + "".join(x) for x in zip_longest(*tmp, fillvalue="") + )[::-1] if len(decimal_part) > 0: ret += '.' ret += decimal_part @@ -506,6 +514,7 @@ def suffix_string_to_number(in_str: str) -> Optional[int]: >>> suffix_string_to_number('13.1Gb') 14066017894 """ + def suffix_capitalize(s: str) -> str: if len(s) == 1: return s.upper() @@ -791,6 +800,9 @@ def extract_mac_address(in_str: Any, *, separator: str = ":") -> Optional[str]: >>> extract_mac_address(' MAC Address: 34:29:8F:12:0D:2F') '34:29:8F:12:0D:2F' + >>> extract_mac_address('? (10.0.0.30) at d8:5d:e2:34:54:86 on em0 expires in 1176 seconds [ethernet]') + 'd8:5d:e2:34:54:86' + """ if not is_full_string(in_str): return None @@ -858,16 +870,16 @@ def words_count(in_str: str) -> int: return len(WORDS_COUNT_RE.findall(in_str)) -def generate_uuid(as_hex: bool = False) -> str: +def generate_uuid(omit_dashes: bool = False) -> str: """ Generated an UUID string (using `uuid.uuid4()`). generate_uuid() # possible output: '97e3a716-6b33-4ab9-9bb1-8128cb24d76b' - generate_uuid(as_hex=True) # possible output: '97e3a7166b334ab99bb18128cb24d76b' + generate_uuid(omit_dashes=True) # possible output: '97e3a7166b334ab99bb18128cb24d76b' """ uid = uuid4() - if as_hex: + if omit_dashes: return uid.hex return str(uid) @@ -1089,12 +1101,14 @@ def to_date(in_str: str) -> Optional[datetime.date]: Parses a date string. See DateParser docs for details. """ import dateparse.dateparse_utils as dp + try: d = dp.DateParser() d.parse(in_str) return d.get_date() except dp.ParseException: - logger.warning(f'Unable to parse date {in_str}.') + msg = f'Unable to parse date {in_str}.' + logger.warning(msg) return None @@ -1103,12 +1117,14 @@ def valid_date(in_str: str) -> bool: True if the string represents a valid date. """ import dateparse.dateparse_utils as dp + try: d = dp.DateParser() _ = d.parse(in_str) return True except dp.ParseException: - logger.warning(f'Unable to parse date {in_str}.') + msg = f'Unable to parse date {in_str}.' + logger.warning(msg) return False @@ -1117,13 +1133,15 @@ def to_datetime(in_str: str) -> Optional[datetime.datetime]: Parses a datetime string. See DateParser docs for more info. """ import dateparse.dateparse_utils as dp + try: d = dp.DateParser() dt = d.parse(in_str) if type(dt) == datetime.datetime: return dt except ValueError: - logger.warning(f'Unable to parse datetime {in_str}.') + msg = f'Unable to parse datetime {in_str}.' + logger.warning(msg) return None @@ -1134,10 +1152,29 @@ def valid_datetime(in_str: str) -> bool: _ = to_datetime(in_str) if _ is not None: return True - logger.warning(f'Unable to parse datetime {in_str}.') + msg = f'Unable to parse datetime {in_str}.' + logger.warning(msg) return False +def squeeze(in_str: str, character_to_squeeze: str = ' ') -> str: + """ + Squeeze runs of more than one character_to_squeeze into one. + + >>> squeeze(' this is a test ') + ' this is a test ' + + >>> squeeze('one|!||!|two|!||!|three', character_to_squeeze='|!|') + 'one|!|two|!|three' + + """ + return re.sub( + r'(' + re.escape(character_to_squeeze) + r')+', + character_to_squeeze, + in_str, + ) + + def dedent(in_str: str) -> str: """ Removes tab indentation from multi line strings (inspired by analogous Scala function). @@ -1206,6 +1243,7 @@ class SprintfStdout(object): 'test\n' """ + def __init__(self) -> None: self.destination = io.StringIO() self.recorder = None @@ -1305,9 +1343,7 @@ def trigrams(txt: str): def shuffle_columns_into_list( - input_lines: Iterable[str], - column_specs: Iterable[Iterable[int]], - delim='' + input_lines: Iterable[str], column_specs: Iterable[Iterable[int]], delim='' ) -> Iterable[str]: """Helper to shuffle / parse columnar data and return the results as a list. The column_specs argument is an iterable collection of @@ -1337,9 +1373,9 @@ def shuffle_columns_into_list( def shuffle_columns_into_dict( - input_lines: Iterable[str], - column_specs: Iterable[Tuple[str, Iterable[int]]], - delim='' + input_lines: Iterable[str], + column_specs: Iterable[Tuple[str, Iterable[int]]], + delim='', ) -> Dict[str, str]: """Helper to shuffle / parse columnar data and return the results as a dict. @@ -1445,13 +1481,16 @@ def chunk(txt: str, chunk_size): """ if len(txt) % chunk_size != 0: - logger.warning( - f'String to chunk\'s length ({len(txt)} is not an even multiple of chunk_size ({chunk_size})') + msg = f'String to chunk\'s length ({len(txt)} is not an even multiple of chunk_size ({chunk_size})' + logger.warning(msg) + warnings.warn(msg, stacklevel=2) for x in range(0, len(txt), chunk_size): - yield txt[x:x+chunk_size] + yield txt[x : x + chunk_size] -def to_bitstring(txt: str, *, delimiter='', encoding='utf-8', errors='surrogatepass') -> str: +def to_bitstring( + txt: str, *, delimiter='', encoding='utf-8', errors='surrogatepass' +) -> str: """Encode txt and then chop it into bytes. Note: only bitstrings with delimiter='' are interpretable by from_bitstring. @@ -1466,12 +1505,7 @@ def to_bitstring(txt: str, *, delimiter='', encoding='utf-8', errors='surrogatep """ etxt = to_ascii(txt) - bits = bin( - int.from_bytes( - etxt, - 'big' - ) - ) + bits = bin(int.from_bytes(etxt, 'big')) bits = bits[2:] return delimiter.join(chunk(bits.zfill(8 * ((len(bits) + 7) // 8)), 8)) @@ -1497,9 +1531,58 @@ def from_bitstring(bits: str, encoding='utf-8', errors='surrogatepass') -> str: """ n = int(bits, 2) - return n.to_bytes((n.bit_length() + 7) // 8, 'big').decode(encoding, errors) or '\0' + return ( + n.to_bytes((n.bit_length() + 7) // 8, 'big').decode(encoding, errors) + or '\0' + ) + + +def ip_v4_sort_key(txt: str) -> Tuple[int]: + """Turn an IPv4 address into a tuple for sorting purposes. + + >>> ip_v4_sort_key('10.0.0.18') + (10, 0, 0, 18) + + >>> ips = ['10.0.0.10', '100.0.0.1', '1.2.3.4', '10.0.0.9'] + >>> sorted(ips, key=lambda x: ip_v4_sort_key(x)) + ['1.2.3.4', '10.0.0.9', '10.0.0.10', '100.0.0.1'] + + """ + if not is_ip_v4(txt): + print(f"not IP: {txt}") + return None + return tuple([int(x) for x in txt.split('.')]) + + +def path_ancestors_before_descendants_sort_key(volume: str) -> Tuple[str]: + """Chunk up a file path so that parent/ancestor paths sort before + children/descendant paths. + + >>> path_ancestors_before_descendants_sort_key('/usr/local/bin') + ('usr', 'local', 'bin') + + >>> paths = ['/usr/local', '/usr/local/bin', '/usr'] + >>> sorted(paths, key=lambda x: path_ancestors_before_descendants_sort_key(x)) + ['/usr', '/usr/local', '/usr/local/bin'] + + """ + return tuple([x for x in volume.split('/') if len(x) > 0]) + + +def replace_all(in_str: str, replace_set: str, replacement: str) -> str: + """Execute several replace operations in a row. + + >>> s = 'this_is a-test!' + >>> replace_all(s, ' _-!', '') + 'thisisatest' + + """ + for char in replace_set: + in_str = in_str.replace(char, replacement) + return in_str if __name__ == '__main__': import doctest + doctest.testmod()