|
|
import re |
|
|
from typing import Any, List, Tuple |
|
|
|
|
|
from .text_utils import construct_dict_str |
|
|
|
|
|
indx = re.compile(r"^(\d+)$") |
|
|
|
|
|
|
|
|
def is_index(string): |
|
|
return bool(indx.match(string)) |
|
|
|
|
|
|
|
|
name = re.compile(r"^[\w. -]+$") |
|
|
|
|
|
|
|
|
def is_name(string): |
|
|
return bool(name.match(string)) |
|
|
|
|
|
|
|
|
def is_wildcard(string): |
|
|
return string == "*" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_query_and_break_to_components( |
|
|
query: str, allow_int_index=True |
|
|
) -> List[str]: |
|
|
if not isinstance(query, str) or len(query) == 0: |
|
|
raise ValueError( |
|
|
f"invalid query: either not a string or an empty string: {query}" |
|
|
) |
|
|
query = query.replace("//", "/").strip() |
|
|
if query.startswith("/"): |
|
|
query = query[1:] |
|
|
|
|
|
|
|
|
if query.endswith("/"): |
|
|
query = query + "*" |
|
|
|
|
|
|
|
|
components = query.split("/") |
|
|
components = [component.strip() for component in components] |
|
|
for component in components: |
|
|
if not ( |
|
|
is_name(component) |
|
|
or is_wildcard(component) |
|
|
or (is_index(component) and allow_int_index) |
|
|
): |
|
|
raise ValueError( |
|
|
f"Component {component} in input query is none of: valid field-name, non-neg-int, or '*'" |
|
|
) |
|
|
return components |
|
|
|
|
|
|
|
|
def is_subpath(subpath, fullpath, allow_int_index=True): |
|
|
|
|
|
subpath_components = validate_query_and_break_to_components( |
|
|
subpath, allow_int_index=allow_int_index |
|
|
) |
|
|
fullpath_components = validate_query_and_break_to_components( |
|
|
fullpath, allow_int_index=allow_int_index |
|
|
) |
|
|
|
|
|
|
|
|
return fullpath_components[: len(subpath_components)] == subpath_components |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_values( |
|
|
current_element: Any, |
|
|
query: List[str], |
|
|
index_into_query: int, |
|
|
remove_empty_ancestors=False, |
|
|
allow_int_index=True, |
|
|
) -> Tuple[bool, Any]: |
|
|
component = query[index_into_query] |
|
|
if index_into_query == -1: |
|
|
if is_wildcard(component): |
|
|
|
|
|
current_element = [] if isinstance(current_element, list) else {} |
|
|
return (True, current_element) |
|
|
|
|
|
|
|
|
if is_index(component) and allow_int_index: |
|
|
component = int(component) |
|
|
try: |
|
|
current_element.pop(component) |
|
|
return (True, current_element) |
|
|
except: |
|
|
|
|
|
return (False, None) |
|
|
|
|
|
|
|
|
if component == "*": |
|
|
|
|
|
|
|
|
if isinstance(current_element, dict): |
|
|
key_values = list(current_element.items()) |
|
|
keys, values = zip(*key_values) |
|
|
elif isinstance(current_element, list): |
|
|
keys = list(range(len(current_element))) |
|
|
values = current_element |
|
|
else: |
|
|
return (False, None) |
|
|
|
|
|
any_success = False |
|
|
for i in range( |
|
|
len(keys) - 1, -1, -1 |
|
|
): |
|
|
try: |
|
|
success, new_val = delete_values( |
|
|
current_element=values[i], |
|
|
query=query, |
|
|
index_into_query=index_into_query + 1, |
|
|
remove_empty_ancestors=remove_empty_ancestors, |
|
|
allow_int_index=allow_int_index, |
|
|
) |
|
|
if not success: |
|
|
continue |
|
|
any_success = True |
|
|
if (len(new_val) == 0) and remove_empty_ancestors: |
|
|
current_element.pop(keys[i]) |
|
|
else: |
|
|
current_element[keys[i]] = new_val |
|
|
|
|
|
except: |
|
|
continue |
|
|
return (any_success, current_element) |
|
|
|
|
|
|
|
|
if is_index(component) and allow_int_index: |
|
|
component = int(component) |
|
|
try: |
|
|
success, new_val = delete_values( |
|
|
current_element=current_element[component], |
|
|
query=query, |
|
|
index_into_query=index_into_query + 1, |
|
|
remove_empty_ancestors=remove_empty_ancestors, |
|
|
allow_int_index=allow_int_index, |
|
|
) |
|
|
if not success: |
|
|
return (False, None) |
|
|
if (len(new_val) == 0) and remove_empty_ancestors: |
|
|
current_element.pop(component) |
|
|
else: |
|
|
current_element[component] = new_val |
|
|
return (True, current_element) |
|
|
except: |
|
|
return (False, None) |
|
|
|
|
|
|
|
|
def dict_delete( |
|
|
dic: dict, |
|
|
query: str, |
|
|
not_exist_ok: bool = False, |
|
|
remove_empty_ancestors=False, |
|
|
allow_int_index=True, |
|
|
): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if dic is None or not isinstance(dic, (list, dict)): |
|
|
raise ValueError( |
|
|
f"dic {dic} is either None or not a list nor a dict. Can not delete from it." |
|
|
) |
|
|
|
|
|
if len(query) == 0: |
|
|
raise ValueError( |
|
|
"Query is an empty string, implying the deletion of dic as a whole. This can not be done via this function call." |
|
|
) |
|
|
|
|
|
if isinstance(dic, dict) and query.strip() in dic: |
|
|
dic.pop(query.strip()) |
|
|
return |
|
|
|
|
|
qpath = validate_query_and_break_to_components( |
|
|
query, allow_int_index=allow_int_index |
|
|
) |
|
|
|
|
|
try: |
|
|
success, new_val = delete_values( |
|
|
current_element=dic, |
|
|
query=qpath, |
|
|
index_into_query=(-1) * len(qpath), |
|
|
remove_empty_ancestors=remove_empty_ancestors, |
|
|
allow_int_index=allow_int_index, |
|
|
) |
|
|
|
|
|
if success: |
|
|
if new_val == {}: |
|
|
dic.clear() |
|
|
return |
|
|
|
|
|
if not not_exist_ok: |
|
|
raise ValueError( |
|
|
f"An attempt to delete from dictionary {dic}, an element {query}, that does not exist in the dictionary, while not_exist_ok=False" |
|
|
) |
|
|
|
|
|
except Exception as e: |
|
|
raise ValueError(f"query {query} matches no path in dictionary {dic}") from e |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_values( |
|
|
current_element: Any, |
|
|
query: List[str], |
|
|
index_into_query: int, |
|
|
allow_int_index=True, |
|
|
) -> Tuple[bool, Any]: |
|
|
|
|
|
if index_into_query == 0: |
|
|
return (True, current_element) |
|
|
|
|
|
|
|
|
component = query[index_into_query] |
|
|
if component == "*": |
|
|
|
|
|
if not isinstance(current_element, (list, dict)): |
|
|
return (False, None) |
|
|
to_ret = [] |
|
|
if isinstance(current_element, dict): |
|
|
sub_elements = list(current_element.values()) |
|
|
else: |
|
|
sub_elements = current_element |
|
|
for sub_element in sub_elements: |
|
|
try: |
|
|
success, val = get_values( |
|
|
sub_element, |
|
|
query, |
|
|
index_into_query + 1, |
|
|
allow_int_index=allow_int_index, |
|
|
) |
|
|
if success: |
|
|
to_ret.append(val) |
|
|
except: |
|
|
continue |
|
|
|
|
|
return (len(to_ret) > 0 or index_into_query == -1, to_ret) |
|
|
|
|
|
|
|
|
if is_index(component) and allow_int_index: |
|
|
component = int(component) |
|
|
try: |
|
|
success, new_val = get_values( |
|
|
current_element[component], |
|
|
query, |
|
|
index_into_query + 1, |
|
|
allow_int_index=allow_int_index, |
|
|
) |
|
|
if success: |
|
|
return (True, new_val) |
|
|
return (False, None) |
|
|
except: |
|
|
return (False, None) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_values( |
|
|
current_element: Any, |
|
|
value: Any, |
|
|
index_into_query: int, |
|
|
fixed_parameters: dict, |
|
|
set_multiple: bool = False, |
|
|
allow_int_index=True, |
|
|
) -> Tuple[bool, Any]: |
|
|
if index_into_query == 0: |
|
|
return (True, value) |
|
|
|
|
|
|
|
|
if current_element is not None and not isinstance(current_element, (list, dict)): |
|
|
current_element = None |
|
|
|
|
|
if current_element is None and not fixed_parameters["generate_if_not_exists"]: |
|
|
return (False, None) |
|
|
|
|
|
component = fixed_parameters["query"][index_into_query] |
|
|
if component == "*": |
|
|
if current_element is not None and set_multiple: |
|
|
if isinstance(current_element, dict) and len(current_element) != len(value): |
|
|
return (False, None) |
|
|
if isinstance(current_element, list) and len(current_element) > len(value): |
|
|
return (False, None) |
|
|
if len(current_element) < len(value): |
|
|
if not fixed_parameters["generate_if_not_exists"]: |
|
|
return (False, None) |
|
|
|
|
|
current_element.extend([None] * (len(value) - len(current_element))) |
|
|
if current_element is None or current_element == []: |
|
|
current_element = [None] * ( |
|
|
len(value) |
|
|
if set_multiple |
|
|
else value is None |
|
|
or not isinstance(value, list) |
|
|
or len(value) > 0 |
|
|
or index_into_query < -1 |
|
|
) |
|
|
|
|
|
if isinstance(current_element, dict): |
|
|
keys = sorted(current_element.keys()) |
|
|
else: |
|
|
keys = list(range(len(current_element))) |
|
|
|
|
|
any_success = False |
|
|
for i in range(len(keys)): |
|
|
try: |
|
|
success, new_val = set_values( |
|
|
current_element=current_element[keys[i]], |
|
|
value=value[i] if set_multiple else value, |
|
|
index_into_query=index_into_query + 1, |
|
|
set_multiple=False, |
|
|
fixed_parameters=fixed_parameters, |
|
|
allow_int_index=allow_int_index, |
|
|
) |
|
|
if not success: |
|
|
continue |
|
|
any_success = True |
|
|
current_element[keys[i]] = new_val |
|
|
|
|
|
except: |
|
|
continue |
|
|
return ( |
|
|
any_success or (len(keys) == 0 and index_into_query == -1), |
|
|
current_element, |
|
|
) |
|
|
|
|
|
|
|
|
if is_index(component) and allow_int_index: |
|
|
if current_element is None or not isinstance(current_element, list): |
|
|
if not fixed_parameters["generate_if_not_exists"]: |
|
|
return (False, None) |
|
|
current_element = [] |
|
|
|
|
|
component = int(component) |
|
|
if component >= len(current_element): |
|
|
if not fixed_parameters["generate_if_not_exists"]: |
|
|
return (False, None) |
|
|
|
|
|
current_element.extend([None] * (component + 1 - len(current_element))) |
|
|
next_current_element = current_element[component] |
|
|
else: |
|
|
if current_element is None or not isinstance(current_element, dict): |
|
|
if not fixed_parameters["generate_if_not_exists"]: |
|
|
return (False, None) |
|
|
current_element = {} |
|
|
if ( |
|
|
component not in current_element |
|
|
and not fixed_parameters["generate_if_not_exists"] |
|
|
): |
|
|
return (False, None) |
|
|
next_current_element = ( |
|
|
None if component not in current_element else current_element[component] |
|
|
) |
|
|
try: |
|
|
success, new_val = set_values( |
|
|
current_element=next_current_element, |
|
|
value=value, |
|
|
index_into_query=index_into_query + 1, |
|
|
fixed_parameters=fixed_parameters, |
|
|
set_multiple=set_multiple, |
|
|
allow_int_index=allow_int_index, |
|
|
) |
|
|
if success: |
|
|
current_element[component] = new_val |
|
|
return (True, current_element) |
|
|
return (False, None) |
|
|
except: |
|
|
return (False, None) |
|
|
|
|
|
|
|
|
|
|
|
def dict_get( |
|
|
dic: dict, |
|
|
query: str, |
|
|
not_exist_ok: bool = False, |
|
|
default: Any = None, |
|
|
allow_int_index=True, |
|
|
): |
|
|
if len(query.strip()) == 0: |
|
|
return dic |
|
|
|
|
|
if dic is None: |
|
|
raise ValueError("Can not get any value from a dic that is None") |
|
|
|
|
|
if isinstance(dic, dict) and query.strip() in dic: |
|
|
return dic[query.strip()] |
|
|
|
|
|
components = validate_query_and_break_to_components( |
|
|
query, allow_int_index=allow_int_index |
|
|
) |
|
|
if len(components) > 1: |
|
|
try: |
|
|
success, values = get_values( |
|
|
dic, components, -1 * len(components), allow_int_index=allow_int_index |
|
|
) |
|
|
if success: |
|
|
return values |
|
|
except Exception as e: |
|
|
raise ValueError( |
|
|
f'query "{query}" did not match any item in dict:\n{construct_dict_str(dic)}' |
|
|
) from e |
|
|
|
|
|
if not_exist_ok: |
|
|
return default |
|
|
|
|
|
raise ValueError( |
|
|
f'query "{query}" did not match any item in dict:\n{construct_dict_str(dic)}' |
|
|
) |
|
|
|
|
|
|
|
|
if components[0] in dic: |
|
|
return dic[components[0]] |
|
|
|
|
|
if not_exist_ok: |
|
|
return default |
|
|
|
|
|
raise ValueError( |
|
|
f'query "{query}" did not match any item in dict:\n{construct_dict_str(dic)}' |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def dict_set( |
|
|
dic: dict, |
|
|
query: str, |
|
|
value: Any, |
|
|
not_exist_ok=True, |
|
|
set_multiple=False, |
|
|
allow_int_index=True, |
|
|
): |
|
|
if dic is None or not isinstance(dic, (list, dict)): |
|
|
raise ValueError( |
|
|
f"Can not change dic that is either None or not a dict nor a list. Got dic = {dic}" |
|
|
) |
|
|
|
|
|
if query.strip() == "": |
|
|
|
|
|
if isinstance(dic, dict): |
|
|
if value is None or not isinstance(value, dict): |
|
|
raise ValueError( |
|
|
f"Through an empty query, trying to set a whole new value, {value}, to the whole of dic, {dic}, but value is not a dict" |
|
|
) |
|
|
dic.clear() |
|
|
dic.update(value) |
|
|
return |
|
|
|
|
|
if isinstance(dic, list): |
|
|
if value is None or not isinstance(value, list): |
|
|
raise ValueError( |
|
|
f"Through an empty query, trying to set a whole new value, {value}, to the whole of dic, {dic}, but value is not a list" |
|
|
) |
|
|
dic.clear() |
|
|
dic.extend(value) |
|
|
return |
|
|
|
|
|
if isinstance(dic, dict) and query.strip() in dic: |
|
|
dic[query.strip()] = value |
|
|
return |
|
|
|
|
|
if set_multiple: |
|
|
if value is None or not isinstance(value, list) or len(value) == 0: |
|
|
raise ValueError( |
|
|
f"set_multiple=True, but value, {value}, can not be broken up, as either it is not a list or it is an empty list" |
|
|
) |
|
|
|
|
|
components = validate_query_and_break_to_components( |
|
|
query, allow_int_index=allow_int_index |
|
|
) |
|
|
fixed_parameters = { |
|
|
"query": components, |
|
|
"generate_if_not_exists": not_exist_ok, |
|
|
} |
|
|
try: |
|
|
success, val = set_values( |
|
|
current_element=dic, |
|
|
value=value, |
|
|
index_into_query=(-1) * len(components), |
|
|
fixed_parameters=fixed_parameters, |
|
|
set_multiple=set_multiple, |
|
|
allow_int_index=allow_int_index, |
|
|
) |
|
|
if not success and not not_exist_ok: |
|
|
raise ValueError(f"No path in dic {dic} matches query {query}.") |
|
|
|
|
|
except Exception as e: |
|
|
raise ValueError(f"No path in dic {dic} matches query {query}.") from e |
|
|
|