Skip to content

Commit

Permalink
feat: add matchers for ISO 8601 date format
Browse files Browse the repository at this point in the history
This introduces pact.Format.iso_8601_datetime() method to match a
string for a full ISO 8601 Date.

This method differs does not do any sort of date validation, only
checks if the string is according to the ISO 8601 spec.

This method differs from `pact.Format.timestamp`,
`~pact.Format.date` and `pact.Format.time` implementations in that
it is more stringent and tests the string for exact match to the
ISO 8601 dates format.

Without `with_millis` parameter will match string containing ISO
8601 formatted dates as stated bellow:

* 2016-12-15T20:16:01
* 2010-05-01T01:14:31.876
* 2016-05-24T15:54:14.00000Z
* 1994-11-05T08:15:30-05:00
* 2002-01-31T23:00:00.1234-02:00
* 1991-02-20T06:35:26.079043+00:00

Otherwise, ONLY dates with milliseconds will match the pattern:

* 2010-05-01T01:14:31.876
* 2016-05-24T15:54:14.00000Z
* 2002-01-31T23:00:00.1234-02:00
* 1991-02-20T06:35:26.079043+00:00

This change aims to bring the capabilities of the python library into
alignment with pact-foundation/docs.pact.io#88, since the existing
functionality is a bit liberal and allows tests to pass even in cases
where the dates do not conform to the ISO 8601 spec.
  • Loading branch information
sergeyklay committed Mar 12, 2023
1 parent 48b7f37 commit 01ec353
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 13 deletions.
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,23 +257,25 @@ Often times, you find yourself having to re-write regular expressions for common
```python
from pact import Format
Format().integer # Matches if the value is an integer
Format().ip_address # Matches if the value is a ip address
Format().ip_address # Matches if the value is an ip address
```

We've created a number of them for you to save you the time:

| matcher | description |
|-----------------|-------------------------------------------------------------------------------------------------|
| `identifier` | Match an ID (e.g. 42) |
| `integer` | Match all numbers that are integers (both ints and longs) |
| `decimal` | Match all real numbers (floating point and decimal) |
| `hexadecimal` | Match all hexadecimal encoded strings |
| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) |
| `timestamp` | Match a string containing an RFC3339 formatted timestapm (e.g. Mon, 31 Oct 2016 15:21:41 -0400) |
| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) |
| `ip_address` | Match string containing IP4 formatted address |
| `ipv6_address` | Match string containing IP6 formatted address |
| `uuid` | Match strings containing UUIDs |
| matcher | description |
|----------------------|-------------------------------------------------------------------------------------------------------------------------|
| `identifier` | Match an ID (e.g. 42) |
| `integer` | Match all numbers that are integers (both ints and longs) |
| `decimal` | Match all real numbers (floating point and decimal) |
| `hexadecimal` | Match all hexadecimal encoded strings |
| `date` | Match string containing basic ISO8601 dates (e.g. 2016-01-01) |
| `timestamp` | Match a string containing an RFC3339 formatted timestamp (e.g. Mon, 31 Oct 2016 15:21:41 -0400) |
| `time` | Match string containing times in ISO date format (e.g. T22:44:30.652Z) |
| `iso_datetime` | Match string containing ISO 8601 formatted dates (e.g. 2015-08-06T16:53:10+01:00) |
| `iso_datetime_mills` | Match string containing ISO 8601 formatted dates, enforcing millisecond precision (e.g. 2015-08-06T16:53:10.123+01:00) |
| `ip_address` | Match string containing IP4 formatted address |
| `ipv6_address` | Match string containing IP6 formatted address |
| `uuid` | Match strings containing UUIDs |

These can be used to replace other matchers

Expand Down
52 changes: 52 additions & 0 deletions pact/matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,8 @@ def __init__(self):
self.timestamp = self.timestamp()
self.date = self.date()
self.time = self.time()
self.iso_datetime = self.iso_8601_datetime()
self.iso_datetime_mills = self.iso_8601_datetime(with_millis=True)

def integer_or_identifier(self):
"""
Expand Down Expand Up @@ -360,6 +362,52 @@ def time(self):
).time().isoformat()
)

def iso_8601_datetime(self, with_millis=False):
"""
Match a string for a full ISO 8601 Date.
Does not do any sort of date validation, only checks if the string is
according to the ISO 8601 spec.
This method differs from :func:`~pact.Format.timestamp`,
:func:`~pact.Format.date` and :func:`~pact.Format.time` implementations
in that it is more stringent and tests the string for exact match to
the ISO 8601 dates format.
Without `with_millis` will match string containing ISO 8601
formatted dates as stated bellow:
* 2016-12-15T20:16:01
* 2010-05-01T01:14:31.876
* 2016-05-24T15:54:14.00000Z
* 1994-11-05T08:15:30-05:00
* 2002-01-31T23:00:00.1234-02:00
* 1991-02-20T06:35:26.079043+00:00
Otherwise, ONLY dates with milliseconds will match the pattern:
* 2010-05-01T01:14:31.876
* 2016-05-24T15:54:14.00000Z
* 2002-01-31T23:00:00.1234-02:00
* 1991-02-20T06:35:26.079043+00:00
:param with_millis: Enforcing millisecond precision.
:type with_millis: bool
:return: a Term object with a date regex.
:rtype: Term
"""
date = [1991, 2, 20, 6, 35, 26]
if with_millis:
matcher = self.Regexes.iso_8601_datetime_mils.value
date.append(79043)
else:
matcher = self.Regexes.iso_8601_datetime.value

return Term(
matcher,
datetime.datetime(*date, tzinfo=datetime.timezone.utc).isoformat()
)

class Regexes(Enum):
"""Regex Enum for common formats."""

Expand Down Expand Up @@ -398,3 +446,7 @@ class Regexes(Enum):
r'0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|' \
r'[12]\d{2}|3([0-5]\d|6[1-6])))?)'
time_regex = r'^(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?$'
iso_8601_datetime = r'^\d{4}-[01]\d-[0-3]\d\x54[0-2]\d:[0-6]\d:' \
r'[0-6]\d(?:\.\d+)?(?:(?:[+-]\d\d:\d\d)|\x5A)?$'
iso_8601_datetime_mils = r'^\d{4}-[01]\d-[0-3]\d\x54[0-2]\d:[0-6]\d:' \
r'[0-6]\d\.\d+(?:(?:[+-]\d\d:\d\d)|\x5A)?$'
42 changes: 42 additions & 0 deletions tests/test_matchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,45 @@ def test_time(self):
},
},
)

def test_iso_8601_datetime(self):
date = self.formatter.iso_datetime.generate()
self.assertEqual(
date,
{
"json_class": "Pact::Term",
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.iso_8601_datetime.value,
"o": 0,
},
"generate": datetime.datetime(
1991, 2, 20, 6, 35, 26, 79043,
tzinfo=datetime.timezone.utc
).isoformat(),
},
},
)

def test_iso_8601_datetime_mills(self):
date = self.formatter.iso_datetime_mills.generate()
self.assertEqual(
date,
{
"json_class": "Pact::Term",
"json_class": "Pact::Term",
"data": {
"matcher": {
"json_class": "Regexp",
"s": self.formatter.Regexes.iso_8601_datetime_mils.value,
"o": 0,
},
"generate": datetime.datetime(
1991, 2, 20, 6, 35, 26, 79043,
tzinfo=datetime.timezone.utc
).isoformat(),
},
},
)

0 comments on commit 01ec353

Please sign in to comment.