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: allow decimal places for seconds setting in NOW replacement #416

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ v3.3.2

*Release date: In development*

- Allow setting the number of decimal places for seconds when using the `[NOW]` replacement with a specific format

v3.3.1
------

Expand Down
6 changes: 3 additions & 3 deletions docs/bdd_integration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ functions or check the :ref:`dataset <dataset>` module for more implementation d
* :code:`[RANDOM]`: Generates a random value
* :code:`[RANDOM_PHONE_NUMBER]`: Generates a random phone number for language and country configured in dataset.language and dataset.country
* :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:`[DATETIME]`: Generates a datetime from the current time (UTC)
* :code:`[NOW]`: Similar to DATETIME without microseconds; 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. When using the %f placeholder, the number of digits to be used can be set like this: %3f
* :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
Expand Down
33 changes: 24 additions & 9 deletions toolium/test/utils/test_dataset_replace_param.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"""

import datetime
import re
from uuid import UUID

import pytest
Expand Down Expand Up @@ -182,18 +183,18 @@ def test_replace_param_datetime_language_ignored():

def test_replace_param_today_spanish():
param = replace_param('[TODAY]', language='es')
assert param == datetime.datetime.today().strftime('%d/%m/%Y')
assert param == datetime.datetime.utcnow().strftime('%d/%m/%Y')


def test_replace_param_today_not_spanish():
param = replace_param('[TODAY]', language='en')
assert param == datetime.datetime.today().strftime('%Y/%m/%d')
assert param == datetime.datetime.utcnow().strftime('%Y/%m/%d')


def test_replace_param_today_offset():
param = replace_param('[TODAY - 1 DAYS]', language='es')
assert param == datetime.datetime.strftime(
datetime.datetime.today() - datetime.timedelta(days=1), '%d/%m/%Y')
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%d/%m/%Y')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

este método está deprecado hace tiempo en python, la recomendación es usar
.now(datetime.timezone.utc)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ídem, también me fijé y también lo vi como un cambio que se podría hacer de manera generalizada en otra PR si queremos seguir la recomendación.



def test_replace_param_now_spanish():
Expand All @@ -211,6 +212,20 @@ def test_replace_param_now_with_format():
assert param == datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')


def test_replace_param_now_with_format_and_decimals_limit():
param = replace_param('[NOW(%Y-%m-%dT%H:%M:%S.%3fZ)]')
param_till_dot = param[:param.find('.')]
assert param_till_dot == datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

estos tests funcionan siempre (la pregunta aplica a todos, incluidos los anteriores)? el now del replace param y esta llamada pueden caer en un segundo distinto y fallará el test no?
creo que sería mejor mockear datetime.datetime.utcnow

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sí, lo pensé cuando los metí, pero he visto que tardan del orden de microsegundos en ejecutarse y en la práctica no fallan nunca.

Con todo, si quieres, se puede meter lo que dices, pero casi preferiría que fuera en otra PR y aquí seguir una aproximación más continuista para no meter más cambios.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Como curiosidad, los que sí fallaban siempre entre las 00:00 y la 01:00 o entre las 00:00 y las 02:00 (según la época del año) cuando se ejecutaban en una máquina con hora española, eran los que usaban today(), porque tiene en cuenta la zona horaria, mientras nuestros replacements usan UTC.

No preguntéis cómo me di cuenta 😆

assert re.match(param_till_dot + r'\.\d{3}Z', param)


def test_replace_param_now_with_format_and_decimals_limit_beyond_microseconds():
param = replace_param('[NOW(%Y-%m-%dT%H:%M:%S.%12fZ)]')
param_till_dot = param[:param.find('.')]
assert param_till_dot == datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S')
assert re.match(param_till_dot + r'\.\d{12}Z', param)


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)]'
Expand All @@ -236,14 +251,14 @@ def test_replace_param_now_offset_with_format():
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(
datetime.datetime.today() - datetime.timedelta(days=1), '%d/%m/%Y')
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%d/%m/%Y')
assert param == f'The day {offset_date} was yesterday'


def test_replace_param_today_offset_and_more_not_spanish():
param = replace_param('The day [TODAY - 1 DAYS] was yesterday', language='it')
offset_date = datetime.datetime.strftime(
datetime.datetime.today() - datetime.timedelta(days=1), '%Y/%m/%d')
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%Y/%m/%d')
assert param == f'The day {offset_date} was yesterday'


Expand Down Expand Up @@ -285,29 +300,29 @@ def test_replace_param_today_offset_with_format_and_more_with_extra_spaces():
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(
datetime.datetime.today() - datetime.timedelta(days=1), '%d/%m/%Y')
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%d/%m/%Y')
assert param == f'The day {offset_date} was yesterday'


def test_replace_param_today_offset_and_more_at_the_end():
param = replace_param('Yesterday was [TODAY - 1 DAYS]', language='es')
offset_date = datetime.datetime.strftime(
datetime.datetime.today() - datetime.timedelta(days=1), '%d/%m/%Y')
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%d/%m/%Y')
assert param == f'Yesterday was {offset_date}'


def test_replace_param_today_offset_and_more_at_the_beginning():
param = replace_param('[TODAY - 1 DAYS] is yesterday', language='es')
offset_date = datetime.datetime.strftime(
datetime.datetime.today() - datetime.timedelta(days=1), '%d/%m/%Y')
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%d/%m/%Y')
assert param == f'{offset_date} is yesterday'


def test_replace_param_today_offsets_and_more():
param = replace_param(
'The day [TODAY - 1 DAYS] was yesterday and I have an appointment at [NOW + 10 MINUTES]', language='es')
offset_date = datetime.datetime.strftime(
datetime.datetime.today() - datetime.timedelta(days=1), '%d/%m/%Y')
datetime.datetime.utcnow() - datetime.timedelta(days=1), '%d/%m/%Y')
offset_datetime = datetime.datetime.strftime(
datetime.datetime.utcnow() + datetime.timedelta(minutes=10), '%d/%m/%Y %H:%M:%S')
assert param == f'The day {offset_date} was yesterday and I have an appointment at {offset_datetime}'
Expand Down
54 changes: 34 additions & 20 deletions toolium/utils/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ def replace_param(param, language='es', infer_param_type=True):
- [RANDOM_PHONE_NUMBER] Generates a random phone number for language and country configured
in dataset.language and dataset.country
- [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
- [DATETIME] Generates a datetime from the current time (UTC)
- [NOW] Similar to DATETIME without microseconds; 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
the datetime module. In the case of the %f placeholder, the syntax has been extended to allow setting
an arbitrary number of digits (e.g. %3f would leave just the 3 most significant digits and truncate the rest)
- [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
Expand Down Expand Up @@ -298,13 +299,38 @@ def _get_substring_replacement(type_mapping_match_group):
return replace_param


def _get_format_with_number_of_decimals(base, language):
"""
Get the format and the number of decimals from the base string.
"""
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'

format_matcher = re.search(r'\((.*)\)', base)
if format_matcher and len(format_matcher.groups()) == 1:
time_format = format_matcher.group(1)
decimal_matcher = re.search(r'%(\d+)f', time_format)
if decimal_matcher and len(decimal_matcher.groups()) == 1:
return time_format.replace(decimal_matcher.group(0), '%f'), int(decimal_matcher.group(1))
return time_format, None
return _default_format(base), None


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
[datetime.strftime](https://docs.python.org/3/library/datetime.html#datetime.date.strftime) function.
In the case of the %f placeholder, the syntax has been extended to allow setting an arbitrary number of digits
(e.g. %3f would leave just the 3 most significant digits and truncate the rest).

:param param: parameter value
:param language: language to configure date format for NOW and TODAY
Expand All @@ -321,28 +347,16 @@ def _offset_datetime(amount, units):
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)
format_str, number_of_decimals = _get_format_with_number_of_decimals(base, language)
date = _offset_datetime(amount, units)
if number_of_decimals:
decimals = f"{date.microsecond / 1_000_000:.{number_of_decimals}f}"[2:]
format_str = format_str.replace("%f", decimals)
return date.strftime(format_str), True


Expand Down