Skip to content

Commit

Permalink
chore: permit custom format for the NOW placeholder (#285)
Browse files Browse the repository at this point in the history
* chore: permit custom format for the NOW placeholder

Signed-off-by: Roberto Mier Escandon <roberto.mierescandon@telefonica.com>

* doc: update replace_param() docstring

* doc: update changedoc

Signed-off-by: Roberto Mier Escandon <roberto.mierescandon@telefonica.com>

* fix: address comments

reword date_matcher
get rid of ISO references in documentation and use strftime function format instead
reduce complexity on _default_format() inner funct

* chore: remove quotes from requirements on dates

NOW('format') has been simplified to NOW(format)

* doc: updated changelog

* doc: complete documentation

Co-authored-by: Roberto Mier Escandon <roberto.mierescandon@telefonica.com>
  • Loading branch information
rmescandon and robertomier committed Apr 28, 2022
1 parent be8db0a commit abad7fb
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ v2.5.1
- Update map_param method to allow recursive replacements
- Update replace_param function to allow multiple date expressions in the same param
- Fix error message to show parent locator instead of object reference when an element is not found
- Update replace_param function to allow NOW datetime expressions with arbitrary formats accepted by datetime.strftime

v2.5.0
------
Expand Down
2 changes: 2 additions & 0 deletions docs/bdd_integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,10 @@ functions or check the :ref:`dataset <dataset>` module for more implementation d
* :code:`[TIMESTAMP]`: Generates a timestamp from the current time
* :code:`[DATETIME]`: Generates a datetime from the current time
* :code:`[NOW]`: Similar to DATETIME without milliseconds; the format depends on the language
* :code:`[NOW(%Y-%m-%dT%H:%M:%SZ)]`: Same as NOW but using an specific format by the python strftime function of the datetime module
* :code:`[NOW + 2 DAYS]`: Similar to NOW but two days later
* :code:`[NOW - 1 MINUTES]`: Similar to NOW but one minute earlier
* :code:`[NOW(%Y-%m-%dT%H:%M:%SZ) - 7 DAYS]`: Similar to NOW but seven days before and with the indicated format
* :code:`[TODAY]`: Similar to NOW without time; the format depends on the language
* :code:`[TODAY + 2 DAYS]`: Similar to NOW, but two days later
* :code:`[STR:xxxx]`: Cast xxxx to a string
Expand Down
64 changes: 64 additions & 0 deletions toolium/test/utils/test_dataset_replace_param.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,33 @@ def test_replace_param_now_not_spanish():
assert param == datetime.datetime.utcnow().strftime('%Y/%m/%d %H:%M:%S')


def test_replace_param_now_with_format():
param = replace_param('[NOW(%Y-%m-%dT%H:%M:%SZ)]')
assert param == datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')


def test_not_replace_param_now_with_invalid_opening_parenthesis_in_format():
param = replace_param('[NOW(%Y-%m-%dT(%H:%M:%SZ)]')
assert param == '[NOW(%Y-%m-%dT(%H:%M:%SZ)]'


def test_not_replace_param_now_with_invalid_closing_parenthesis_in_format():
param = replace_param('[NOW(%Y-%m-%dT)%H:%M:%SZ)]')
assert param == '[NOW(%Y-%m-%dT)%H:%M:%SZ)]'


def test_replace_param_now_offset():
param = replace_param('[NOW + 5 MINUTES]', language='es')
assert param == datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(minutes=5), '%d/%m/%Y %H:%M:%S')


def test_replace_param_now_offset_with_format():
param = replace_param('[NOW(%Y-%m-%dT%H:%M:%SZ) + 5 MINUTES]')
assert param == datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(minutes=5), '%Y-%m-%dT%H:%M:%SZ')


def test_replace_param_today_offset_and_more():
param = replace_param('The day [TODAY - 1 DAYS] was yesterday', language='es')
offset_date = datetime.datetime.strftime(
Expand Down Expand Up @@ -222,6 +243,27 @@ def test_replace_param_now_offset_and_more_not_spanish():
assert param == f'I will arrive at {offset_datetime} tomorrow'


def test_replace_param_now_offset_with_format_and_more():
param = replace_param('I will arrive at [NOW(%Y-%m-%dT%H:%M:%SZ) + 10 MINUTES] tomorrow')
offset_datetime = datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(minutes=10), '%Y-%m-%dT%H:%M:%SZ')
assert param == f'I will arrive at {offset_datetime} tomorrow'


def test_replace_param_now_offset_with_format_and_more_language_is_irrelevant():
param = replace_param('I will arrive at [NOW(%Y-%m-%dT%H:%M:%SZ) + 10 MINUTES] tomorrow', language='ru')
offset_datetime = datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(minutes=10), '%Y-%m-%dT%H:%M:%SZ')
assert param == f'I will arrive at {offset_datetime} tomorrow'


def test_replace_param_today_offset_with_format_and_more_with_extra_spaces():
param = replace_param('The date [NOW(%Y-%m-%dT%H:%M:%SZ) - 1 DAYS ] was yesterday')
offset_date = datetime.datetime.strftime(
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%Y-%m-%dT%H:%M:%SZ')
assert param == f'The date {offset_date} was yesterday'


def test_replace_param_today_offset_and_more_with_extra_spaces():
param = replace_param('The day [TODAY - 1 DAYS ] was yesterday', language='es')
offset_date = datetime.datetime.strftime(
Expand Down Expand Up @@ -253,6 +295,28 @@ def test_replace_param_today_offsets_and_more():
assert param == f'The day {offset_date} was yesterday and I have an appointment at {offset_datetime}'


def test_replace_param_now_offsets_with_format_and_more():
param = replace_param(
'The date [NOW(%Y-%m-%dT%H:%M:%SZ) - 1 DAYS] was yesterday '
'and I have an appointment at [NOW(%Y-%m-%dT%H:%M:%SZ) + 10 MINUTES]')
offset_date = datetime.datetime.strftime(
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%Y-%m-%dT%H:%M:%SZ')
offset_datetime = datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(minutes=10), '%Y-%m-%dT%H:%M:%SZ')
assert param == f'The date {offset_date} was yesterday and I have an appointment at {offset_datetime}'


def test_replace_param_now_offsets_with_and_without_format_and_more():
param = replace_param(
'The date [NOW(%Y-%m-%dT%H:%M:%SZ) - 1 DAYS] was yesterday '
'and I have an appointment at [NOW + 10 MINUTES]', language='es')
offset_date = datetime.datetime.strftime(
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%Y-%m-%dT%H:%M:%SZ')
offset_datetime = datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(minutes=10), '%d/%m/%Y %H:%M:%S')
assert param == f'The date {offset_date} was yesterday and I have an appointment at {offset_datetime}'


def test_replace_param_str_int():
param = replace_param('[STR:28]')
assert type(param) == str
Expand Down
82 changes: 61 additions & 21 deletions toolium/utils/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ def replace_param(param, language='es', infer_param_type=True):
[TIMESTAMP] Generates a timestamp from the current time
[DATETIME] Generates a datetime from the current time
[NOW] Similar to DATETIME without milliseconds; the format depends on the language
[NOW(%Y-%m-%dT%H:%M:%SZ)] Same as NOW but using an specific format by the python strftime function of the datetime module
[NOW + 2 DAYS] Similar to NOW but two days later
[NOW - 1 MINUTES] Similar to NOW but one minute earlier
[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
[STR:xxxx] Cast xxxx to a string
Expand Down Expand Up @@ -140,15 +142,30 @@ def _replace_param_type(param):

def _find_param_date_expressions(param):
"""
Finds in a param a date expression.
For example, for a param like "it happened on [NOW - 1 MONTH] of the last year", this method returns
the string "[NOW - 1 MONTH]"
Finds in a param one or several date expressions.
For example, for a param like "it happened on [NOW - 1 MONTH] of the last year and will happen [TODAY('%d/%m')]",
this method returns an array with two string elements: "[NOW - 1 MONTH]" and [TODAY('%d/%m')]"
The kind of expressions to search are based on these rules:
- expression is sorrounded by [ and ]
- first word of the expression is either NOW or TODAY
- when first word is NOW, it can have an addtional format for the date between parenthesis,
like NOW(%Y-%m-%dT%H:%M:%SZ). The definition of the format is the same as considered by the
python strftime function of the datetime module
- and optional offset can be given by indicating how many days, hours, etc.. to add or remove to the current datetime.
This part of the expression includes a +/- symbol plus a number and a unit
Some valid expressions are:
[NOW]
[TODAY]
[NOW(%Y-%m-%dT%H:%M:%SZ)]
[NOW(%Y-%m-%dT%H:%M:%SZ) - 180 DAYS]
[NOW(%H:%M:%S) + 4 MINUTES]
:param param: parameter value
:param language: language to configure date format for NOW and TODAY
:return: the specific text within the param holding a date expression
:return: An array with all the matching date expressions found in the param
"""
return re.findall(r'\[(?:NOW|TODAY)\s*[\+|-]\s*\d+\s*\w+\s*\]', param)
return re.findall(r"\[(?:NOW(?:\((?:[^\(\)]*)\))?|TODAY)(?:\s*[\+|-]\s*\d+\s*\w+\s*)?\]", param)


def _replace_param_replacement(param, language):
Expand Down Expand Up @@ -179,7 +196,7 @@ def _replace_param_replacement(param, language):
date_expressions = _find_param_date_expressions(param)
for date_expr in date_expressions:
replacements[date_expr] = _replace_param_date(date_expr, language)[0]

new_param = param
param_replaced = False
for key in replacements.keys():
Expand Down Expand Up @@ -221,25 +238,48 @@ def _replace_param_date(param, language):
"""
Transform param value in a date after applying the specified delta.
E.g. [TODAY - 2 DAYS], [NOW - 10 MINUTES]
An specific format could be defined in the case of NOW this way: NOW('THEFORMAT')
where THEFORMAT is any valid format accepted by the python
[datetime.strftime](https://docs.python.org/3/library/datetime.html#datetime.date.strftime) function
:param param: parameter value
:param language: language to configure date format for NOW and TODAY
:return: tuple with replaced value and boolean to know if replacement has been done
"""
date_format = '%d/%m/%Y %H:%M:%S' if language == 'es' else '%Y/%m/%d %H:%M:%S'
date_day_format = '%d/%m/%Y' if language == 'es' else '%Y/%m/%d'
date_matcher = re.match(r'\[(NOW|TODAY)\s*([\+|-]\s*\d+)\s*(\w+)\s*\]', param)
new_param = param
param_replaced = False

if date_matcher and len(date_matcher.groups()) == 3:
configuration = dict([(date_matcher.group(3).lower(), int(date_matcher.group(2).replace(' ', '')))])
now = (date_matcher.group(1) == 'NOW')
reference_date = datetime.datetime.utcnow() if now else datetime.datetime.utcnow().date()
replace_value = reference_date + datetime.timedelta(**configuration)
new_param = replace_value.strftime(date_format) if now else replace_value.strftime(date_day_format)
param_replaced = True
return new_param, param_replaced
def _date_matcher():
return re.match(r'\[(NOW(?:\((?:.*)\)|)|TODAY)(?:\s*([\+|-]\s*\d+)\s*(\w+)\s*)?\]', param)

def _offset_datetime(amount, units):
now = datetime.datetime.utcnow()
if not amount or not units:
return now
the_amount = int(amount.replace(' ',''))
the_units = units.lower()
return now + datetime.timedelta(**dict([(the_units, the_amount)]))

def _is_only_date(base):
return 'TODAY' in base

def _default_format(base):
date_format = '%d/%m/%Y' if language == 'es' else '%Y/%m/%d'
if _is_only_date(base):
return date_format
return f'{date_format} %H:%M:%S'

def _get_format(base):
format_matcher = re.match(r'.*\((.*)\).*', base)
if format_matcher and len(format_matcher.groups()) == 1:
return format_matcher.group(1)
return _default_format(base)

matcher = _date_matcher()
if not matcher:
return param, False

base, amount, units = list(matcher.groups())
format_str = _get_format(base)
date = _offset_datetime(amount, units)
return date.strftime(format_str), True


def _replace_param_fixed_length(param):
Expand Down

0 comments on commit abad7fb

Please sign in to comment.