Skip to content

Commit

Permalink
feat: improve and add new replacements to the dataset module (#403)
Browse files Browse the repository at this point in the history
* Update utils datasets functions

* Fix linter issues

* Fix linter issue C901

* Fix linter issues

* Fix linter issues

* Fix linter issues

* Restore some changes

* Restore some changes

* Remove test

* Remove test

* Remove requirement

* Fix linter issues

* Fix tests issue

* Update CHANGELOG.rst

Co-authored-by: Pablo Guijarro <[email protected]>

* Update toolium/utils/dataset.py

Co-authored-by: Pablo Guijarro <[email protected]>

* Fix pull request issues

* Fix pull request issues

* Restore incorrect pr requested change

* Remove unrequired tests

* Fix pull request issue

* Fix typo

* Fix linter issues

* Fix test issue

* Fix linter issues

* Fix tests issue

* Update CHANGELOG.rst

Co-authored-by: Pablo Guijarro <[email protected]>

* Update toolium/test/utils/test_dataset_replace_param.py

Co-authored-by: Pablo Guijarro <[email protected]>

* Update toolium/utils/dataset.py

Co-authored-by: Pablo Guijarro <[email protected]>

* Fix pull request issue

* Fix pull request issue

* Fix linter issues

* Fix linter issues

* Fix pull request issues

* Fix issue

* Fix linter issues

* Fix fuction issue

* Fix issue

* Fix pull request issues

* Fix pull request issues

* Update toolium/test/utils/test_dataset_map_param_context.py

Co-authored-by: Pablo Guijarro <[email protected]>

* Update toolium/test/utils/test_dataset_replace_param.py

Co-authored-by: Pablo Guijarro <[email protected]>

* Reorder tests

* Restore missing tests

* Fix pull request issue

* Fix pull request issue

* Include json option to list element

* Fix tests issue

* Fix pull request issue

---------

Co-authored-by: Pablo Guijarro <[email protected]>
  • Loading branch information
promans718 and pabloge authored Oct 10, 2024
1 parent 7d868a1 commit 5699865
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ target
.vscode

# virtualenv
.venv
venv/
VENV/
ENV/
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ v3.2.1
------

*Release date: In development*
- Allow negative indexes for list elements in context searches. Example: [CONTEXT:element.-1]
- Add support for JSON strings to the `DICT` and `LIST`` replacement. Example: [DICT:{"key": true}], [LIST:[null]]
- Add `REPLACE` replacement, to replace a substring with another. Example: [REPLACE:[CONTEXT:some_url]::https::http]
- Add `TITLE` replacement, to apply Python's title() function. Example: [TITLE:the title]
- Add `ROUND` replacement, float number to a string with the indicated number of decimals. Example: [ROUND:3.3333::2]

v3.2.0
------
Expand Down
59 changes: 58 additions & 1 deletion toolium/test/utils/test_dataset_map_param_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,64 @@ class Context(object):
assert map_param("[CONTEXT:list.cmsScrollableActions.1.id]") == 'ask-for-qa'


def test_a_context_param_list_correct_negative_index():
"""
Verification of a list with a correct negative index (In bounds) as CONTEXT
"""
class Context(object):
pass
context = Context()

context.list = {
'cmsScrollableActions': [
{
'id': 'ask-for-duplicate',
'text': 'QA duplica'
},
{
'id': 'ask-for-qa',
'text': 'QA no duplica'
},
{
'id': 'ask-for-negative',
'text': 'QA negative index'
}
]
}
dataset.behave_context = context
assert map_param("[CONTEXT:list.cmsScrollableActions.-1.id]") == 'ask-for-negative'
assert map_param("[CONTEXT:list.cmsScrollableActions.-3.id]") == 'ask-for-duplicate'


def test_a_context_param_list_incorrect_negative_index():
"""
Verification of a list with a incorrect negative index (In bounds) as CONTEXT
"""
class Context(object):
pass
context = Context()

context.list = {
'cmsScrollableActions': [
{
'id': 'ask-for-duplicate',
'text': 'QA duplica'
},
{
'id': 'ask-for-qa',
'text': 'QA no duplica'
},
{
'id': 'ask-for-negative',
'text': 'QA negative index'
}
]
}
with pytest.raises(Exception) as excinfo:
map_param("[CONTEXT:list.cmsScrollableActions.-5.id]")
assert "the expression '-5' was not able to select an element in the list" == str(excinfo.value)


def test_a_context_param_list_oob_index():
"""
Verification of a list with an incorrect index (Out of bounds) as CONTEXT
Expand Down Expand Up @@ -511,7 +569,6 @@ def __init__(self):
context.list = ExampleClass()
dataset.behave_context = context

print(context)
with pytest.raises(Exception) as excinfo:
map_param("[CONTEXT:list.cmsScrollableActions.prueba.id]")
assert "the expression 'prueba' was not able to select an element in the list" == str(excinfo.value)
Expand Down
40 changes: 40 additions & 0 deletions toolium/test/utils/test_dataset_replace_param.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,20 @@ def test_replace_param_now_offsets_with_and_without_format_and_more():
assert param == f'The date {offset_date} was yesterday and I have an appointment at {offset_datetime}'


def test_replace_param_round_with_type_inference():
param = replace_param('[ROUND:7.5::2]')
assert param == 7.5
param = replace_param('[ROUND:3.33333333::3]')
assert param == 3.333


def test_replace_param_round_without_type_inference():
param = replace_param('[ROUND:7.500::2]', infer_param_type=False)
assert param == '7.50'
param = replace_param('[ROUND:3.33333333::3]', infer_param_type=False)
assert param == '3.333'


def test_replace_param_str_int():
param = replace_param('[STR:28]')
assert isinstance(param, str)
Expand Down Expand Up @@ -369,12 +383,22 @@ def test_replace_param_list_strings():
assert param == ['1', '2', '3']


def test_replace_param_list_json_format():
param = replace_param('[LIST:["value", true, null]]')
assert param == ["value", True, None]


def test_replace_param_dict():
param = replace_param("[DICT:{'a':'test1','b':'test2','c':'test3'}]")
assert isinstance(param, dict)
assert param == {'a': 'test1', 'b': 'test2', 'c': 'test3'}


def test_replace_param_dict_json_format():
param = replace_param('[DICT:{"key": "value", "key_2": true, "key_3": null}]')
assert param == {"key": "value", "key_2": True, "key_3": None}


def test_replace_param_upper():
param = replace_param('[UPPER:test]')
assert param == 'TEST'
Expand Down Expand Up @@ -438,3 +462,19 @@ def test_replace_param_partial_string_with_length():
assert param == 'aaaaa is string'
param = replace_param('parameter [STRING_WITH_LENGTH_5] is string')
assert param == 'parameter aaaaa is string'


def test_replace_param_replace():
param = replace_param('[REPLACE:https://url.com::https::http]')
assert param == "http://url.com"
param = replace_param('[REPLACE:https://url.com::https://]')
assert param == "url.com"


def test_replace_param_title():
param = replace_param('[TITLE:hola hola]')
assert param == "Hola Hola"
param = replace_param('[TITLE:holahola]')
assert param == "Holahola"
param = replace_param('[TITLE:hOlA]')
assert param == "HOlA"
82 changes: 64 additions & 18 deletions toolium/utils/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,16 @@ def replace_param(param, language='es', infer_param_type=True):
[NOW(%Y-%m-%dT%H:%M:%SZ) - 7 DAYS] Similar to NOW but seven days before and with the indicated format
[TODAY] Similar to NOW without time; the format depends on the language
[TODAY + 2 DAYS] Similar to NOW, but two days later
[ROUND:xxxx::y] Generates a string from a float number (xxxx) with the indicated number of decimals (y)
[STR:xxxx] Cast xxxx to a string
[INT:xxxx] Cast xxxx to an int
[FLOAT:xxxx] Cast xxxx to a float
[LIST:xxxx] Cast xxxx to a list
[DICT:xxxx] Cast xxxx to a dict
[UPPER:xxxx] Converts xxxx to upper case
[LOWER:xxxx] Converts xxxx to lower case
[REPLACE:xxxxx::yy::zz] Replace elements in string. Example: [REPLACE:[CONTEXT:some_url]::https::http]
[TITLE:xxxxx] Apply .title() to string value. Example: [TITLE:the title]
If infer_param_type is True and the result of the replacement process is a string,
this function also tries to infer and cast the result to the most appropriate data type,
attempting first the direct conversion to a Python built-in data type and then,
Expand Down Expand Up @@ -181,7 +184,7 @@ def _replace_param_replacement(param, language):
"""
Replace param with a new param value.
Available replacements: [EMPTY], [B], [UUID], [RANDOM], [RANDOM_PHONE_NUMBER],
[TIMESTAMP], [DATETIME], [NOW], [TODAY]
[TIMESTAMP], [DATETIME], [NOW], [TODAY], [ROUND:xxxxx::d]
:param param: parameter value
:param language: language to configure date format for NOW and TODAY
Expand All @@ -200,7 +203,8 @@ def _replace_param_replacement(param, language):
'[TIMESTAMP]': str(int(datetime.datetime.timestamp(datetime.datetime.utcnow()))),
'[DATETIME]': str(datetime.datetime.utcnow()),
'[NOW]': str(datetime.datetime.utcnow().strftime(date_format)),
'[TODAY]': str(datetime.datetime.utcnow().strftime(date_day_format))
'[TODAY]': str(datetime.datetime.utcnow().strftime(date_day_format)),
r'\[ROUND:(.*?)::(\d*)\]': _get_rounded_float_number
}

# append date expressions found in param to the replacement dict
Expand All @@ -210,14 +214,28 @@ def _replace_param_replacement(param, language):

new_param = param
param_replaced = False
for key in replacements.keys():
if key in new_param:
new_value = replacements[key]() if isfunction(replacements[key]) else replacements[key]
new_param = new_param.replace(key, new_value)
for key, value in replacements.items():
if key.startswith('['): # tags without placeholders
if key in new_param:
new_value = value() if isfunction(value) else value
new_param = new_param.replace(key, new_value)
param_replaced = True
elif match := re.search(key, new_param): # tags with placeholders
new_value = value(match) # a function to parse the values is always required
new_param = new_param.replace(match.group(), new_value)
param_replaced = True
return new_param, param_replaced


def _get_rounded_float_number(match):
"""
Round float number with the expected decimals
:param match: match object of the regex for this transformation: [ROUND:(.*?)::(d*)]
:return: float as string with the expected decimals
"""
return f"{round(float(match.group(1)), int(match.group(2))):.{int(match.group(2))}f}"


def _get_random_phone_number():
# Method to avoid executing data generator when it is not needed
return DataGenerator().phone_number
Expand All @@ -226,31 +244,58 @@ def _get_random_phone_number():
def _replace_param_transform_string(param):
"""
Transform param value according to the specified prefix.
Available transformations: DICT, LIST, INT, FLOAT, STR, UPPER, LOWER
Available transformations: DICT, LIST, INT, FLOAT, STR, UPPER, LOWER, REPLACE, TITLE
:param param: parameter value
:return: tuple with replaced value and boolean to know if replacement has been done
"""
type_mapping_regex = r'\[(DICT|LIST|INT|FLOAT|STR|UPPER|LOWER):(.*)\]'
type_mapping_regex = r'\[(DICT|LIST|INT|FLOAT|STR|UPPER|LOWER|REPLACE|TITLE):([\w\W]*)\]'
type_mapping_match_group = re.match(type_mapping_regex, param)
new_param = param
param_transformed = False

if type_mapping_match_group:
param_transformed = True
if type_mapping_match_group.group(1) == 'STR':
new_param = type_mapping_match_group.group(2)
elif type_mapping_match_group.group(1) in ['LIST', 'DICT', 'INT', 'FLOAT']:
exec('exec_param = {type}({value})'.format(type=type_mapping_match_group.group(1).lower(),
value=type_mapping_match_group.group(2)))
if type_mapping_match_group.group(1) in ['DICT', 'LIST']:
try:
new_param = json.loads(type_mapping_match_group.group(2).strip())
except json.decoder.JSONDecodeError:
new_param = eval(type_mapping_match_group.group(2))
elif type_mapping_match_group.group(1) in ['INT', 'FLOAT']:
exec(f'exec_param = {type_mapping_match_group.group(1).lower()}({type_mapping_match_group.group(2)})')
new_param = locals()['exec_param']
elif type_mapping_match_group.group(1) == 'UPPER':
new_param = type_mapping_match_group.group(2).upper()
elif type_mapping_match_group.group(1) == 'LOWER':
new_param = type_mapping_match_group.group(2).lower()
else:
replace_param = _get_substring_replacement(type_mapping_match_group)
new_param = new_param.replace(type_mapping_match_group.group(), replace_param)
return new_param, param_transformed


def _get_substring_replacement(type_mapping_match_group):
"""
Transform param value according to the specified prefix.
Available transformations: STR, UPPER, LOWER, REPLACE, TITLE
:param type_mapping_match_group: match group
:return: return the string with the replaced param
"""
if type_mapping_match_group.group(1) == 'STR':
replace_param = type_mapping_match_group.group(2)
elif type_mapping_match_group.group(1) == 'UPPER':
replace_param = type_mapping_match_group.group(2).upper()
elif type_mapping_match_group.group(1) == 'LOWER':
replace_param = type_mapping_match_group.group(2).lower()
elif type_mapping_match_group.group(1) == 'REPLACE':
params_to_replace = type_mapping_match_group.group(2).split('::')
replace_param = params_to_replace[2] if len(params_to_replace) > 2 else ''
param_to_replace = params_to_replace[1] if params_to_replace[1] != '\\n' else '\n'
param_to_replace = params_to_replace[1] if params_to_replace[1] != '\\r' else '\r'
replace_param = params_to_replace[0].replace(param_to_replace, replace_param)
elif type_mapping_match_group.group(1) == 'TITLE':
replace_param = "".join(map(min, zip(type_mapping_match_group.group(2),
type_mapping_match_group.group(2).title())))
return replace_param


def _replace_param_date(param, language):
"""
Transform param value in a date after applying the specified delta.
Expand Down Expand Up @@ -611,7 +656,8 @@ def get_value_from_context(param, context):
if isinstance(value, dict) and part in value:
value = value[part]
# evaluate if in an array, access is requested by index
elif isinstance(value, list) and part.isdigit() and int(part) < len(value):
elif isinstance(value, list) and part.lstrip('-+').isdigit() \
and abs(int(part)) < (len(value) + 1 if part.startswith("-") else len(value)):
value = value[int(part)]
# or by a key=value expression
elif isinstance(value, list) and (element := _select_element_in_list(value, part)):
Expand Down

0 comments on commit 5699865

Please sign in to comment.