Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve and add new replacements to the dataset module #403

Merged
merged 48 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e9132d5
Update utils datasets functions
promans718 Oct 4, 2024
d689d5d
Fix linter issues
promans718 Oct 4, 2024
2977618
Fix linter issue C901
promans718 Oct 4, 2024
7892e31
Fix linter issues
promans718 Oct 4, 2024
c946775
Fix linter issues
promans718 Oct 4, 2024
93c6e44
Fix linter issues
promans718 Oct 4, 2024
e6953e4
Restore some changes
promans718 Oct 4, 2024
833d7c7
Restore some changes
promans718 Oct 4, 2024
0a6ca46
Remove test
promans718 Oct 4, 2024
f2db21c
Remove test
promans718 Oct 4, 2024
98b4509
Remove requirement
promans718 Oct 4, 2024
ecdb84c
Fix linter issues
promans718 Oct 4, 2024
a24e2bf
Fix tests issue
promans718 Oct 4, 2024
87e2bc3
Update CHANGELOG.rst
promans718 Oct 7, 2024
100aea2
Update toolium/utils/dataset.py
promans718 Oct 7, 2024
ca62047
Fix pull request issues
promans718 Oct 7, 2024
4b3161f
Fix pull request issues
promans718 Oct 7, 2024
d3f17a8
Restore incorrect pr requested change
promans718 Oct 7, 2024
858b217
Remove unrequired tests
promans718 Oct 7, 2024
ada58fb
Fix pull request issue
promans718 Oct 8, 2024
6c767a6
Fix typo
promans718 Oct 8, 2024
2f3875c
Fix linter issues
promans718 Oct 8, 2024
80d2f53
Fix test issue
promans718 Oct 8, 2024
2a579a5
Fix linter issues
promans718 Oct 8, 2024
b03b631
Fix tests issue
promans718 Oct 8, 2024
cef0c6f
Update CHANGELOG.rst
promans718 Oct 8, 2024
9bc711b
Update toolium/test/utils/test_dataset_replace_param.py
promans718 Oct 8, 2024
7fc1b51
Update toolium/utils/dataset.py
promans718 Oct 8, 2024
1bcdae4
Fix pull request issue
promans718 Oct 8, 2024
867d8d0
Fix pull request issue
promans718 Oct 8, 2024
d6d2321
Fix linter issues
promans718 Oct 8, 2024
bdf11c7
Fix linter issues
promans718 Oct 8, 2024
281a77e
Fix pull request issues
promans718 Oct 8, 2024
2eba630
Fix issue
promans718 Oct 8, 2024
a53e5c3
Fix linter issues
promans718 Oct 8, 2024
d19b12c
Fix fuction issue
promans718 Oct 8, 2024
384c942
Fix issue
promans718 Oct 9, 2024
715922f
Fix pull request issues
promans718 Oct 9, 2024
0d6cd55
Fix pull request issues
promans718 Oct 9, 2024
39be4e7
Update toolium/test/utils/test_dataset_map_param_context.py
promans718 Oct 9, 2024
8b8be25
Update toolium/test/utils/test_dataset_replace_param.py
promans718 Oct 9, 2024
8a3c240
Reorder tests
promans718 Oct 9, 2024
7a4ff53
Restore missing tests
promans718 Oct 9, 2024
7242af6
Fix pull request issue
promans718 Oct 9, 2024
5512678
Fix pull request issue
promans718 Oct 9, 2024
9fa8610
Include json option to list element
promans718 Oct 9, 2024
4e8d860
Fix tests issue
promans718 Oct 9, 2024
b650606
Fix pull request issue
promans718 Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'
promans718 marked this conversation as resolved.
Show resolved Hide resolved
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'

promans718 marked this conversation as resolved.
Show resolved Hide resolved

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):
promans718 marked this conversation as resolved.
Show resolved Hide resolved
promans718 marked this conversation as resolved.
Show resolved Hide resolved
"""
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
Loading