# -*- coding: utf-8 -*-
# Copyright 2024, SERTIT-ICube - France, https://sertit.unistra.fr/
# This file is part of sertit-utils project
# https://github.com/sertit/sertit-utils
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Tools concerning strings """
import argparse
import logging
import re
import uuid
from datetime import date, datetime
from typing import Any, Union
from sertit.logs import SU_NAME
LOGGER = logging.getLogger(SU_NAME)
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
[docs]
def str_to_bool(bool_str: str) -> bool:
"""
Convert a string to a bool.
Accepted values (compared in lower case):
- :code:`True` <=> :code:`yes`, :code:`true`, :code:`t`, :code:`1`
- :code:`False` <=> :code:`no`, :code:`false`, :code:`f`, :code:`0`
Args:
bool_str: Bool as a string
Returns:
bool: Boolean value
Examples:
>>> str_to_bool("yes") == True # Works with "yes", "true", "t", "y", "1" (accepted with any letter case)
True
>>> str_to_bool("no") == False # Works with "no", "false", "f", "n", "0" (accepted with any letter case)
True
"""
if isinstance(bool_str, bool):
return bool_str
true_str = ("yes", "true", "t", "y", "1")
false_str = ("no", "false", "f", "n", "0")
if bool_str.lower() in true_str:
bool_val = True
elif bool_str.lower() in false_str:
bool_val = False
else:
raise ValueError(
f"Invalid true or false value, "
f"should be {true_str} if True or {false_str} if False, not {bool_str}"
)
return bool_val
[docs]
def str_to_verbosity(verbosity_str: str) -> int:
"""
Return a logging level from a string (compared in lower case).
- :code:`DEBUG` <=> {:code:`debug`, :code:`d`, :code:`10`}
- :code:`INFO` <=> {:code:`info`, :code:`i`, :code:`20`}
- :code:`WARNING` <=> {:code:`warning`, :code:`w`, :code:`warn`}
- :code:`ERROR` <=> {:code:`error`, :code:`e`, :code:`err`}
Args:
verbosity_str (str): String to be converted
Returns:
logging level: Logging level (INFO, DEBUG, WARNING, ERROR)
Examples:
>>> str_to_bool("d") == logging.DEBUG # Works with 'debug', 'd', 10 (accepted with any letter case)
True
>>> str_to_bool("i") == logging.INFO # Works with 'info', 'i', 20 (accepted with any letter case)
True
>>> str_to_bool("w") == logging.WARNING # Works with 'warning', 'w', 'warn', 30 (accepted with any letter case)
True
>>> str_to_bool("e") == logging.ERROR # Works with 'error', 'e', 'err', 40 (accepted with any letter case)
True
"""
debug_str = ("debug", "d", 10)
info_str = ("info", "i", 20)
warn_str = ("warning", "w", "warn", 30)
err_str = ("error", "e", "err", 40)
if isinstance(verbosity_str, str):
verbosity_str = verbosity_str.lower()
if verbosity_str in info_str:
verbosity = logging.INFO
elif verbosity_str in debug_str:
verbosity = logging.DEBUG
elif verbosity_str in warn_str:
verbosity = logging.WARNING
elif verbosity_str in err_str:
verbosity = logging.ERROR
else:
raise argparse.ArgumentTypeError(
f"Incorrect logging level value: {verbosity_str}, "
f"should be {info_str}, {debug_str}, {warn_str} or {err_str}"
)
return verbosity
[docs]
def str_to_list(
list_str: Union[str, list], additional_separator: str = "", case: str = None
) -> list:
"""
Convert str to list with :code:`,`, :code:`;`, :code:`\x20` separators.
Args:
list_str (Union[str, list]): List as a string
additional_separator (str): Additional separators. Base ones are :code:`,`, :code:`;`, :code:`\\\x20` (space).
case (str): {none, 'lower', 'upper'}
Returns:
list: A list from split string
Example:
>>> str_to_list("A, B; C D")
["A", "B", "C", "D"]
"""
if isinstance(list_str, str):
# Concatenate separators
separators = ",|;| "
if additional_separator:
separators += "|" + additional_separator
# Split
listed_str = re.split(separators, list_str)
elif isinstance(list_str, list):
listed_str = list_str
else:
raise ValueError(
f"List should be given as a string or a list of string: {list_str}"
)
out_list = []
for item in listed_str:
# Check if there are null items
if item:
if case == "lower":
item_case = item.lower()
elif case == "upper":
item_case = item.upper()
else:
item_case = item
out_list.append(item_case)
return out_list
[docs]
def str_to_date(
date_str: Union[str, datetime], date_format: str = DATE_FORMAT
) -> datetime:
"""
Convert string to a :code:`datetime.datetime`.
Also accepted date formats:
- :code:`now`: datetime.today()
- Usual JSON date format: :code:`%Y-%m-%d`
- Already formatted datetimes and dates
Args:
date_str (str): Date as a string
date_format (str): Format of the date (as ingested by strptime)
Returns:
datetime.datetime: A date as a python datetime object
Examples:
# Default date format (isoformat)
>>> str_to_date("2020-05-05T08:05:15")
datetime(2020, 5, 5, 8, 5, 15)
# This usual JSON format is also accepted
>>> str_to_date("2019-08-06")
datetime(2019, 8, 6)
# User date's format
>>> str_to_date("20200909105055", date_format="%Y%m%d%H%M%S")
datetime(2020, 9, 9, 10, 50, 55)
"""
if isinstance(date_str, datetime):
dtm = date_str
elif isinstance(date_str, date):
dtm = datetime.fromisoformat(date_str.isoformat())
else:
try:
if date_str.lower() == "now":
# Now with correct format (no microseconds if not specified and so on)
dtm = datetime.strptime(
datetime.today().strftime(date_format), date_format
)
else:
dtm = datetime.strptime(date_str, date_format)
except ValueError:
# Just try with the usual JSON format
json_date_format = "%Y-%m-%d"
try:
dtm = datetime.strptime(date_str, json_date_format)
except ValueError as ex:
raise ValueError(
f"Invalid date format: {date_str}; should be {date_format} "
f"or {json_date_format}"
) from ex
return dtm
[docs]
def str_to_list_of_dates(
date_str: Union[list, str],
date_format: str = DATE_FORMAT,
additional_separator: str = "",
) -> list:
"""
Convert a string containing a list of dates to a list of :code:`datetime.datetime`.
Also accepted date formats:
- :code:`now`: datetime.today()
- Usual JSON date format: :code:`%Y-%m-%d`
- Already formatted datetimes and dates
Args:
date_str (Union[list, str]): Date as a string
date_format (str): Format of the date (as ingested by strptime)
additional_separator (str): Additional separator
Returns:
list: A list containing datetimes objects
Example:
>>> # Default date format (isoformat)
>>> str_to_list_of_dates("20200909105055, 2019-08-06;19560702121212\t2020-08-09",
>>> date_format="%Y%m%d%H%M%S",
>>> additional_separator="\t")
[datetime(2020, 9, 9, 10, 50, 55), datetime(2019, 8, 6), datetime(1956, 7, 2, 12, 12, 12), datetime(2020, 8, 9)]
"""
# Split string to get a list of strings
list_of_dates_str = str_to_list(date_str, additional_separator)
# Convert strings to date
list_of_dates = [str_to_date(dt, date_format) for dt in list_of_dates_str]
return list_of_dates
[docs]
def to_cmd_string(unquoted_str: Any) -> str:
"""
Add quotes around the string in order to make the command understand it's a string
(useful with tricky symbols like :code:`&` or white spaces):
Args:
unquoted_str (str): String to update
Returns:
str: Quoted string
Example:
>>> # This str wont work in the terminal without quotes (because of the &)
>>> pb_str = r"D:/Minab_4-DA&VHR/Minab_4-DA&VHR.shp"
>>> to_cmd_string(pb_str)
"\"D:/Minab_4-DA&VHR/Minab_4-DA&VHR.shp\""
"""
if not isinstance(unquoted_str, str):
unquoted_str = str(unquoted_str)
cmd_str = unquoted_str
if not unquoted_str.startswith('"'):
cmd_str = '"' + cmd_str
if not unquoted_str.endswith('"'):
cmd_str = cmd_str + '"'
return cmd_str
[docs]
def snake_to_camel_case(snake_str: str) -> str:
"""
Convert a :code:`snake_case` string to :code:`CamelCase`.
Args:
snake_str (str): String formatted in snake_case
Returns:
str: String formatted in CamelCase
Example:
>>> snake_to_camel_case("snake_case")
"SnakeCase"
"""
return "".join((w.capitalize() for w in snake_str.split("_")))
[docs]
def camel_to_snake_case(snake_str: str) -> str:
"""
Convert a :code:`CamelCase` string to :code:`snake_case`.
Args:
snake_str (str): String formatted in CamelCase
Returns:
str: String formatted in snake_case
Example:
>>> camel_to_snake_case("CamelCase")
"camel_case"
"""
return "".join(["_" + c.lower() if c.isupper() else c for c in snake_str]).lstrip(
"_"
)
[docs]
def is_uuid(uuid_string, version=4):
"""
Check if uuid_string is a valid UUID
Args:
uuid_string (str): UUID to be tested
version (int): 4 (default), UUID version
Returns:
bool: Valid UUID or not
"""
if not 1 <= version <= 5:
raise ValueError(
f"Illegal version number: {version} (should be between 1 and 5)"
)
try:
uid = uuid.UUID(str(uuid_string), version=version)
return uid.hex == uuid_string.replace("-", "")
except ValueError:
return False