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: add matchers for ISO 8601 date format #333

Merged
merged 1 commit into from
Apr 29, 2023
Merged
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
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_ms` | 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_ms = self.iso_8601_datetime(with_ms=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_ms=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_ms` 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_ms: Enforcing millisecond precision.
:type with_ms: bool
:return: a Term object with a date regex.
:rtype: Term
"""
date = [1991, 2, 20, 6, 35, 26]
if with_ms:
matcher = self.Regexes.iso_8601_datetime_ms.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_ms = 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,
tzinfo=datetime.timezone.utc
).isoformat(),
},
},
)

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