Skip to content

Commit e449efd

Browse files
committedDec 20, 2019
#49: Implemented timedelta validator and checker.
1 parent d474b27 commit e449efd

File tree

2 files changed

+172
-0
lines changed

2 files changed

+172
-0
lines changed
 

‎validator_collection/checkers.py

+49
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,55 @@ def is_timezone(value,
803803
return True
804804

805805

806+
@disable_checker_on_env
807+
def is_timedelta(value,
808+
resolution = None,
809+
**kwargs):
810+
"""Indicate whether ``value`` is a :class:`timedelta <python:datetime.timedelta>`.
811+
812+
.. note::
813+
814+
Acceptable string formats are:
815+
816+
* "HH:MM:SS"
817+
* "X day, HH:MM:SS"
818+
* "X days, HH:MM:SS"
819+
* "HH:MM:SS.us"
820+
* "X day, HH:MM:SS.us"
821+
* "X days, HH:MM:SS.us"
822+
823+
where "us" refer to microseconds.
824+
825+
Shout out to Alex Pitchford for sharing the
826+
`string-parsing regex <http://kbyanc.blogspot.com/2007/08/python-reconstructing-timedeltas-from.html?showComment=1452111163905#c3907051065256615667>`_.
827+
828+
:param value: The value to evaluate.
829+
830+
:param resolution: Indicates the time period resolution represented by ``value``.
831+
Accepts ``'years'``, ``'weeks'``, ``'days'``, ``'hours'``, ``'minutes'``,
832+
``'seconds'``, ``'milliseconds'``, or ``'microseconds'``. Defaults to
833+
``'seconds'``.
834+
:type resolution: :class:`str <python:str>`
835+
836+
:returns: ``True`` if ``value`` is valid, ``False`` if it is not.
837+
:rtype: :class:`bool <python:bool>`
838+
839+
:raises SyntaxError: if ``kwargs`` contains duplicate keyword parameters or duplicates
840+
keyword parameters passed to the underlying validator
841+
842+
"""
843+
try:
844+
value = validators.timedelta(value,
845+
resolution = resolution,
846+
**kwargs)
847+
except SyntaxError as error:
848+
raise error
849+
except Exception:
850+
return False
851+
852+
return True
853+
854+
806855
## NUMBERS
807856

808857
@disable_checker_on_env

‎validator_collection/validators.py

+123
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@
130130
'^(?:(?:[0-9A-Fa-f]{1,4}:){6}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::(?:[0-9A-Fa-f]{1,4}:){5}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,4}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(?:(?:[0-9A-Fa-f]{1,4}:){,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){,6}[0-9A-Fa-f]{1,4})?::)(?:%25(?:[A-Za-z0-9\\-._~]|%[0-9A-Fa-f]{2})+)?$'
131131
)
132132

133+
TIMEDELTA_REGEX = re.compile(r'((?P<days>\d+) days?, )?(?P<hours>\d+):'
134+
r'(?P<minutes>\d+):(?P<seconds>\d+(\.\d+)?)')
135+
133136
# pylint: disable=W0613
134137

135138
## CORE
@@ -1251,6 +1254,126 @@ def timezone(value,
12511254

12521255
return value
12531256

1257+
@disable_on_env
1258+
def timedelta(value,
1259+
allow_empty = False,
1260+
resolution = 'seconds',
1261+
**kwargs):
1262+
"""Validate that ``value`` is a valid :class:`timedelta <python:datetime.timedelta>`.
1263+
1264+
.. note::
1265+
1266+
Expects to receive a value that is either a
1267+
:class:`timedelta <python:datetime.timedelta>`, a numeric value that can
1268+
be coerced to a :class:`timedelta <python:datetime.timedelta>`, or a
1269+
string that can be coerced to a :class:`timedelta <python:datetime.timedelta>`.
1270+
Acceptable string formats are:
1271+
1272+
* "HH:MM:SS"
1273+
* "X day, HH:MM:SS"
1274+
* "X days, HH:MM:SS"
1275+
* "HH:MM:SS.us"
1276+
* "X day, HH:MM:SS.us"
1277+
* "X days, HH:MM:SS.us"
1278+
1279+
where "us" refer to microseconds.
1280+
1281+
Shout out to Alex Pitchford for sharing the
1282+
`string-parsing regex <http://kbyanc.blogspot.com/2007/08/python-reconstructing-timedeltas-from.html?showComment=1452111163905#c3907051065256615667>`_.
1283+
1284+
:param value: The value to validate. Accepts either a numeric value indicating
1285+
a number of seconds or a string indicating an amount of time.
1286+
:type value: :class:`str <python:str>` / :class:`timedelta <python:datetime.timedelta>`
1287+
/ numeric / :obj:`None <python:None>`
1288+
1289+
:param allow_empty: If ``True``, returns :obj:`None <python:None>` if
1290+
``value`` is empty. If ``False``, raises a
1291+
:class:`EmptyValueError <validator_collection.errors.EmptyValueError>`
1292+
if ``value`` is empty. Defaults to ``False``.
1293+
:type allow_empty: :class:`bool <python:bool>`
1294+
1295+
:param resolution: Indicates the time period resolution represented by ``value``.
1296+
Accepts ``'years'``, ``'weeks'``, ``'days'``, ``'hours'``, ``'minutes'``,
1297+
``'seconds'``, ``'milliseconds'``, or ``'microseconds'``. Defaults to
1298+
``'seconds'``.
1299+
:type resolution: :class:`str <python:str>`
1300+
1301+
:returns: ``value`` / :obj:`None <python:None>`
1302+
:rtype: :class:`timedelta <python:datetime.timedelta>` / :obj:`None <python:None>`
1303+
1304+
:raises ValueError: if ``resolution`` is not a valid time period resolution
1305+
:raises EmptyValueError: if ``value`` is empty and ``allow_empty`` is ``False``
1306+
:raises CannotCoerceError: if ``value`` cannot be coerced to
1307+
:class:`timedelta <python:datetime.timedelta>` and is not :obj:`None <python:None>`
1308+
1309+
"""
1310+
# pylint: disable=too-many-branches
1311+
if isinstance(value, datetime_.timedelta):
1312+
return value
1313+
1314+
if not resolution:
1315+
resolution = 'seconds'
1316+
1317+
if resolution not in ['years',
1318+
'weeks',
1319+
'days',
1320+
'hours',
1321+
'minutes',
1322+
'seconds',
1323+
'milliseconds',
1324+
'microseconds']:
1325+
raise ValueError('resolution (%s) not a valid time period resolution' % resolution)
1326+
1327+
timedelta_properties = {}
1328+
1329+
try:
1330+
value = numeric(value,
1331+
allow_empty = allow_empty,
1332+
force_run = True)
1333+
if resolution == 'years':
1334+
resolution = 'days'
1335+
value = value * 365
1336+
elif resolution == 'weeks':
1337+
resolution = 'days'
1338+
value = value * 7
1339+
1340+
timedelta_properties[resolution] = value
1341+
return datetime_.timedelta(**timedelta_properties)
1342+
except errors.CannotCoerceError:
1343+
try:
1344+
value = string(value,
1345+
allow_empty = allow_empty,
1346+
coerce_value = False,
1347+
force_run = True)
1348+
except errors.CannotCoerceError:
1349+
raise errors.CannotCoerceError('value (%s) could not be coerced to a'
1350+
' timedelta' % value)
1351+
1352+
if not value and not allow_empty:
1353+
raise errors.EmptyValueError('value (%s) was empty' % value)
1354+
elif not value:
1355+
return None
1356+
1357+
value = value.lower().strip()
1358+
1359+
is_valid = TIMEDELTA_REGEX.match(value)
1360+
1361+
if not is_valid:
1362+
raise errors.CannotCoerceError('value (%s) could not be coerced to'
1363+
' a timedelta' % value)
1364+
1365+
timedelta_properties = is_valid.groupdict(0)
1366+
for key, sub_value in timedelta_properties.items():
1367+
try:
1368+
timedelta_properties[key] = numeric(sub_value,
1369+
allow_empty = True,
1370+
force_run = True)
1371+
except errors.CannotCoerceError:
1372+
raise errors.CannotCoerceError('value (%s) could not be coerced to a'
1373+
' timedelta' % value)
1374+
1375+
return datetime_.timedelta(**timedelta_properties)
1376+
12541377

12551378
## NUMBERS
12561379

0 commit comments

Comments
 (0)
Please sign in to comment.