Skip to content

Commit 88287da

Browse files
Merge pull request #103 from gridsmartercities/date_validators
Date validators
2 parents e4b4589 + ac229dc commit 88287da

File tree

3 files changed

+152
-54
lines changed

3 files changed

+152
-54
lines changed

aws_lambda_decorators/validators.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Validation rules."""
2+
import datetime
23
import re
34
from schema import SchemaError
45

@@ -304,3 +305,35 @@ def validate(value=None):
304305
return True
305306

306307
return bool(value)
308+
309+
310+
class DateValidator(Validator):
311+
"""Validation rule to check if a string is a valid date according to some format."""
312+
ERROR_MESSAGE = "'{value}' is not a '{condition}' date"
313+
314+
def __init__(self, date_format: str, error_message=None):
315+
"""
316+
Checks if a string is a date with a given format
317+
318+
Args:
319+
date_format (str): The date format to check against
320+
error_message (str): A custom error message to output if validation fails
321+
"""
322+
super().__init__(error_message, date_format)
323+
324+
def validate(self, value=None):
325+
"""
326+
Check if a string is a date with a given format
327+
328+
Args:
329+
value (str): string date to validate against a format
330+
"""
331+
if value is None:
332+
return True
333+
334+
try:
335+
datetime.datetime.strptime(value, self._condition)
336+
except ValueError:
337+
return False
338+
else:
339+
return True

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
LONG_DESCRIPTION = open("README.md").read()
44

55
setup(name="aws-lambda-decorators",
6-
version="0.43",
6+
version="0.44",
77
description="A set of python decorators to simplify aws python lambda development",
88
long_description=LONG_DESCRIPTION,
99
long_description_content_type="text/markdown",

tests/test_decorators.py

Lines changed: 118 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from aws_lambda_decorators.decorators import extract, extract_from_event, extract_from_context, handle_exceptions, \
1414
log, response_body_as_json, extract_from_ssm, validate, handle_all_exceptions, cors
1515
from aws_lambda_decorators.validators import Mandatory, RegexValidator, SchemaValidator, Minimum, Maximum, MaxLength, \
16-
MinLength, Type, EnumValidator, NonEmpty
16+
MinLength, Type, EnumValidator, NonEmpty, DateValidator
1717

1818
TEST_JWT = "eyJraWQiOiJEQlwvK0lGMVptekNWOGNmRE1XVUxBRlBwQnVObW5CU2NcL2RoZ3pnTVhcL2NzPSIsImFsZyI6IlJTMjU2In0." \
1919
"eyJzdWIiOiJhYWRkMWUwZS01ODA3LTQ3NjMtYjFlOC01ODIzYmY2MzFiYjYiLCJhdWQiOiIycjdtMW1mdWFiODg3ZmZvdG9iNWFjcX" \
@@ -1546,58 +1546,6 @@ def handler(event, context, **kwargs): # noqa
15461546
self.assertEqual(HTTPStatus.OK, response["statusCode"])
15471547
self.assertEqual(expected_body, response["body"])
15481548

1549-
1550-
class IsolatedDecoderTests(unittest.TestCase):
1551-
# Tests have been named so they run in a specific order
1552-
1553-
ID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
1554-
1555-
PARAMETERS = [
1556-
Parameter(path="pathParameters/test_id", validators=[Mandatory, RegexValidator(ID_PATTERN)]),
1557-
Parameter(path="body[json]/name", validators=[MaxLength(255)])
1558-
]
1559-
1560-
def test_01_extract_from_event_400(self):
1561-
event = {
1562-
"pathParameters": {}
1563-
}
1564-
1565-
@extract_from_event(parameters=self.PARAMETERS, group_errors=True, allow_none_defaults=False)
1566-
def handler(event, context, **kwargs): # noqa
1567-
return kwargs
1568-
1569-
response = handler(event, None)
1570-
self.assertEqual(HTTPStatus.BAD_REQUEST, response["statusCode"])
1571-
1572-
def test_02_extract_from_event_200(self):
1573-
test_id = str(uuid4())
1574-
1575-
event = {
1576-
"pathParameters": {
1577-
"test_id": test_id
1578-
},
1579-
"body": json.dumps({
1580-
"name": "Gird"
1581-
})
1582-
}
1583-
1584-
@extract_from_event(parameters=self.PARAMETERS, group_errors=True, allow_none_defaults=False)
1585-
def handler(event, context, **kwargs): # noqa
1586-
return {
1587-
"statusCode": HTTPStatus.OK,
1588-
"body": json.dumps(kwargs)
1589-
}
1590-
1591-
expected_body = json.dumps({
1592-
"test_id": test_id,
1593-
"name": "Gird"
1594-
})
1595-
1596-
response = handler(event, None)
1597-
1598-
self.assertEqual(HTTPStatus.OK, response["statusCode"])
1599-
self.assertEqual(expected_body, response["body"])
1600-
16011549
def test_extract_non_empty_parameter(self):
16021550
event = {
16031551
"value": 20
@@ -1665,3 +1613,120 @@ def handler(event, a=None): # noqa: pylint - unused-argument
16651613
"Error validating parameters. Errors: %s",
16661614
[{"a": ["The value was empty"]}]
16671615
)
1616+
1617+
def test_extract_date_parameter(self):
1618+
event = {
1619+
"a": "2001-01-01 00:00:00"
1620+
}
1621+
1622+
@extract([Parameter("/a", "event", validators=[DateValidator("%Y-%m-%d %H:%M:%S")])])
1623+
def handler(event, a=None): # noqa: pylint - unused-argument
1624+
return a
1625+
1626+
response = handler(event)
1627+
self.assertEqual("2001-01-01 00:00:00", response)
1628+
1629+
@patch("aws_lambda_decorators.decorators.LOGGER")
1630+
def test_extract_date_parameter_fails_on_invalid_date(self, mock_logger):
1631+
event = {
1632+
"a": "2001-01-01 35:00:00"
1633+
}
1634+
1635+
@extract([Parameter("/a", "event", validators=[DateValidator("%Y-%m-%d %H:%M:%S")])])
1636+
def handler(event, a=None): # noqa: pylint - unused-argument
1637+
return {}
1638+
1639+
response = handler(event, None)
1640+
1641+
self.assertEqual(400, response["statusCode"])
1642+
self.assertEqual("{\"message\": [{\"a\": [\"'2001-01-01 35:00:00' is not a '%Y-%m-%d %H:%M:%S' date\"]}]}",
1643+
response["body"])
1644+
1645+
mock_logger.error.assert_called_once_with(
1646+
"Error validating parameters. Errors: %s",
1647+
[{"a": ["'2001-01-01 35:00:00' is not a '%Y-%m-%d %H:%M:%S' date"]}]
1648+
)
1649+
1650+
@patch("aws_lambda_decorators.decorators.LOGGER")
1651+
def test_extract_date_parameter_fails_with_custom_error(self, mock_logger):
1652+
event = {
1653+
"a": "2001-01-01 35:00:00"
1654+
}
1655+
1656+
@extract([Parameter("/a", "event", validators=[DateValidator("%Y-%m-%d %H:%M:%S", "Not a valid date!")])])
1657+
def handler(event, a=None): # noqa: pylint - unused-argument
1658+
return {}
1659+
1660+
response = handler(event, None)
1661+
1662+
self.assertEqual(400, response["statusCode"])
1663+
self.assertEqual("{\"message\": [{\"a\": [\"Not a valid date!\"]}]}", response["body"])
1664+
1665+
mock_logger.error.assert_called_once_with(
1666+
"Error validating parameters. Errors: %s",
1667+
[{"a": ["Not a valid date!"]}]
1668+
)
1669+
1670+
def test_extract_date_parameter_valid_on_empty(self):
1671+
event = {
1672+
"a": None
1673+
}
1674+
1675+
@extract([Parameter("/a", "event", validators=[DateValidator("%Y-%m-%d %H:%M:%S")])])
1676+
def handler(event, a=None): # noqa: pylint - unused-argument
1677+
return a
1678+
1679+
response = handler(event)
1680+
self.assertEqual(None, response)
1681+
1682+
1683+
class IsolatedDecoderTests(unittest.TestCase):
1684+
# Tests have been named so they run in a specific order
1685+
1686+
ID_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
1687+
1688+
PARAMETERS = [
1689+
Parameter(path="pathParameters/test_id", validators=[Mandatory, RegexValidator(ID_PATTERN)]),
1690+
Parameter(path="body[json]/name", validators=[MaxLength(255)])
1691+
]
1692+
1693+
def test_01_extract_from_event_400(self):
1694+
event = {
1695+
"pathParameters": {}
1696+
}
1697+
1698+
@extract_from_event(parameters=self.PARAMETERS, group_errors=True, allow_none_defaults=False)
1699+
def handler(event, context, **kwargs): # noqa
1700+
return kwargs
1701+
1702+
response = handler(event, None)
1703+
self.assertEqual(HTTPStatus.BAD_REQUEST, response["statusCode"])
1704+
1705+
def test_02_extract_from_event_200(self):
1706+
test_id = str(uuid4())
1707+
1708+
event = {
1709+
"pathParameters": {
1710+
"test_id": test_id
1711+
},
1712+
"body": json.dumps({
1713+
"name": "Gird"
1714+
})
1715+
}
1716+
1717+
@extract_from_event(parameters=self.PARAMETERS, group_errors=True, allow_none_defaults=False)
1718+
def handler(event, context, **kwargs): # noqa
1719+
return {
1720+
"statusCode": HTTPStatus.OK,
1721+
"body": json.dumps(kwargs)
1722+
}
1723+
1724+
expected_body = json.dumps({
1725+
"test_id": test_id,
1726+
"name": "Gird"
1727+
})
1728+
1729+
response = handler(event, None)
1730+
1731+
self.assertEqual(HTTPStatus.OK, response["statusCode"])
1732+
self.assertEqual(expected_body, response["body"])

0 commit comments

Comments
 (0)