|
130 | 130 | '^(?:(?:[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})+)?$'
|
131 | 131 | )
|
132 | 132 |
|
| 133 | +TIMEDELTA_REGEX = re.compile(r'((?P<days>\d+) days?, )?(?P<hours>\d+):' |
| 134 | + r'(?P<minutes>\d+):(?P<seconds>\d+(\.\d+)?)') |
| 135 | + |
133 | 136 | # pylint: disable=W0613
|
134 | 137 |
|
135 | 138 | ## CORE
|
@@ -1251,6 +1254,126 @@ def timezone(value,
|
1251 | 1254 |
|
1252 | 1255 | return value
|
1253 | 1256 |
|
| 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 | + |
1254 | 1377 |
|
1255 | 1378 | ## NUMBERS
|
1256 | 1379 |
|
|
0 commit comments