# -*- coding: utf-8 -*- import ciso8601 import datetime import sys from generate_test_timestamps import generate_valid_timestamp_and_datetime, generate_invalid_timestamp if sys.version_info.major == 2: # We use unittest2 since it has a backport of the `unittest.TestCase.assertRaisesRegex` method, # which is called `assertRaisesRegexp` in Python 2. This saves us the hassle of monkey-patching # the class ourselves. import unittest2 as unittest else: import unittest class ValidTimestampTestCase(unittest.TestCase): def test_auto_generated_valid_formats(self): for (timestamp, expected_datetime) in generate_valid_timestamp_and_datetime(): try: self.assertEqual(ciso8601.parse_datetime(timestamp), expected_datetime) except Exception: print("Had problems parsing: {timestamp}".format(timestamp=timestamp)) raise def test_parse_as_naive_auto_generated_valid_formats(self): for (timestamp, expected_datetime) in generate_valid_timestamp_and_datetime(): try: self.assertEqual(ciso8601.parse_datetime_as_naive(timestamp), expected_datetime.replace(tzinfo=None)) except Exception: print("Had problems parsing: {timestamp}".format(timestamp=timestamp)) raise def test_excessive_subsecond_precision(self): self.assertEqual( ciso8601.parse_datetime('20140203T103527.234567891234'), datetime.datetime(2014, 2, 3, 10, 35, 27, 234567) ) def test_leap_year(self): # There is nothing unusual about leap years in ISO 8601. # We just want to make sure that they work in general. for leap_year in (1600, 2000, 2016): self.assertEqual( ciso8601.parse_datetime('{}-02-29'.format(leap_year)), datetime.datetime(leap_year, 2, 29, 0, 0, 0, 0) ) def test_special_midnight(self): self.assertEqual( ciso8601.parse_datetime('2014-02-03T24:00:00'), datetime.datetime(2014, 2, 4, 0, 0, 0) ) class InvalidTimestampTestCase(unittest.TestCase): # Many invalid test cases are covered by `test_parse_auto_generated_invalid_formats`, # But it doesn't cover all invalid cases, so we test those here. # See `generate_test_timestamps.generate_invalid_timestamp` for details. def test_parse_auto_generated_invalid_formats(self): for timestamp in generate_invalid_timestamp(): try: with self.assertRaises(ValueError, msg="Timestamp '{0}' was supposed to be invalid, but parsing it didn't raise ValueError.".format(timestamp)): ciso8601.parse_datetime(timestamp) except Exception as exc: print("Timestamp '{0}' was supposed to raise ValueError, but raised {1} instead".format(timestamp, type(exc).__name__)) raise def test_non_ascii_characters(self): if sys.version_info >= (3, 3): self.assertRaisesRegex( ValueError, r"Invalid character while parsing date separator \('-'\) \('🐵', Index: 7\)", ciso8601.parse_datetime, '2019-01🐵01', ) self.assertRaisesRegex( ValueError, r"Invalid character while parsing day \('🐵', Index: 8\)", ciso8601.parse_datetime, '2019-01-🐵', ) else: self.assertRaisesRegex( ValueError, r"Invalid character while parsing date separator \('-'\) \(Index: 7\)", ciso8601.parse_datetime, '2019-01🐵01', ) self.assertRaisesRegex( ValueError, r"Invalid character while parsing day \(Index: 8\)", ciso8601.parse_datetime, '2019-01-🐵', ) def test_invalid_calendar_separator(self): self.assertRaisesRegex( ValueError, r"Invalid character while parsing month", ciso8601.parse_datetime, '2018=01=01', ) self.assertRaisesRegex( ValueError, r"Invalid character while parsing date separator \('-'\) \('=', Index: 7\)", ciso8601.parse_datetime, '2018-01=01', ) self.assertRaisesRegex( ValueError, r"Invalid character while parsing date separator \('-'\) \('0', Index: 7\)", ciso8601.parse_datetime, '2018-0101', ) self.assertRaisesRegex( ValueError, r"Invalid character while parsing day \('-', Index: 6\)", ciso8601.parse_datetime, '201801-01', ) def test_invalid_empty_but_required_fields(self): self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing year. Expected 4 more characters", ciso8601.parse_datetime, '', ) self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing month. Expected 2 more characters", ciso8601.parse_datetime, '2018-', ) self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing day. Expected 2 more characters", ciso8601.parse_datetime, '2018-01-', ) self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing hour. Expected 2 more characters", ciso8601.parse_datetime, '2018-01-01T', ) self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing minute. Expected 2 more characters", ciso8601.parse_datetime, '2018-01-01T00:', ) self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing second. Expected 2 more characters", ciso8601.parse_datetime, '2018-01-01T00:00:', ) self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing subsecond. Expected 1 more character", ciso8601.parse_datetime, '2018-01-01T00:00:00.', ) self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing tz hour. Expected 2 more characters", ciso8601.parse_datetime, '2018-01-01T00:00:00.00+', ) self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing tz minute. Expected 2 more characters", ciso8601.parse_datetime, '2018-01-01T00:00:00.00-00:', ) def test_invalid_day_for_month(self): for non_leap_year in (1700, 1800, 1900, 2014): self.assertRaisesRegex( ValueError, r"day is out of range for month", ciso8601.parse_datetime, '{}-02-29'.format(non_leap_year) ) self.assertRaisesRegex( ValueError, r"day is out of range for month", ciso8601.parse_datetime, '2014-01-32', ) self.assertRaisesRegex( ValueError, r"day is out of range for month", ciso8601.parse_datetime, '2014-06-31', ) self.assertRaisesRegex( ValueError, r"day is out of range for month", ciso8601.parse_datetime, '2014-06-00', ) def test_invalid_yyyymm_format(self): self.assertRaisesRegex( ValueError, r"Unexpected end of string while parsing day. Expected 2 more characters", ciso8601.parse_datetime, '201406', ) def test_invalid_date_and_time_separator(self): self.assertRaisesRegex( ValueError, r"Invalid character while parsing date and time separator \(ie. 'T' or ' '\) \('_', Index: 10\)", ciso8601.parse_datetime, '2018-01-01_00:00:00', ) def test_invalid_hour_24(self): # A value of hour = 24 is only valid in the special case of 24:00:00 self.assertRaisesRegex( ValueError, r"hour must be in 0..23", ciso8601.parse_datetime, '2014-02-03T24:35:27', ) def test_invalid_time_separator(self): self.assertRaisesRegex( ValueError, r"Invalid character while parsing time separator \(':'\) \('=', Index: 16\)", ciso8601.parse_datetime, '2018-01-01T00:00=00' ) self.assertRaisesRegex( ValueError, r"Invalid character while parsing time separator \(':'\) \('0', Index: 16\)", ciso8601.parse_datetime, '2018-01-01T00:0000' ) self.assertRaisesRegex( ValueError, r"Invalid character while parsing second \(':', Index: 15\)", ciso8601.parse_datetime, '2018-01-01T0000:00' ) def test_invalid_tz_minute(self): self.assertRaisesRegex( ValueError, r"tzminute must be in 0..59", ciso8601.parse_datetime, '2018-01-01T00:00:00.00-00:99', ) def test_invalid_tz_offsets_too_large(self): # The Python interpreter crashes if you give the datetime constructor a TZ offset with an absolute value >= 1440 # TODO: Determine whether these are valid ISO 8601 values and therefore whether ciso8601 should support them. self.assertRaisesRegex( ValueError, # Error message differs whether or not we are using pytz or datetime.timezone r"^offset must be a timedelta strictly between" if sys.version_info.major >= 3 else r"\('absolute offset is too large', -5940\)", ciso8601.parse_datetime, '2018-01-01T00:00:00.00-99', ) self.assertRaisesRegex( ValueError, r"tzminute must be in 0..59", ciso8601.parse_datetime, '2018-01-01T00:00:00.00-23:60', ) def test_mixed_basic_and_extended_formats(self): """ Both dates and times have "basic" and "extended" formats. But when you combine them into a datetime, the date and time components must have the same format. """ self.assertRaisesRegex( ValueError, r"Cannot combine \"extended\" date format with \"basic\" time format", ciso8601.parse_datetime, '2014-01-02T010203', ), self.assertRaisesRegex( ValueError, r"Cannot combine \"basic\" date format with \"extended\" time format", ciso8601.parse_datetime, '20140102T01:02:03', ) class Rfc3339TestCase(unittest.TestCase): def test_valid_rfc3339_timestamps(self): """ Validate that valid RFC 3339 datetimes are parseable by parse_rfc3339 and produce the same result as parse_datetime. """ for string in [ '2018-01-02T03:04:05Z', '2018-01-02t03:04:05z', '2018-01-02 03:04:05z', '2018-01-02T03:04:05+00:00', '2018-01-02T03:04:05-00:00', '2018-01-02T03:04:05.12345Z', '2018-01-02T03:04:05+01:23', '2018-01-02T03:04:05-12:34', '2018-01-02T03:04:05-12:34', ]: self.assertEqual(ciso8601.parse_datetime(string), ciso8601.parse_rfc3339(string)) def test_invalid_rfc3339_timestamps(self): """ Validate that datetime strings that are valid ISO 8601 but invalid RFC 3339 trigger a ValueError when passed to RFC 3339, and that this ValueError explicitly mentions RFC 3339. """ for timestamp in [ "2018-01-02", # Missing mandatory time "2018-01-02T03", # Missing mandatory minute and second "2018-01-02T03Z", # Missing mandatory minute and second "2018-01-02T03:04", # Missing mandatory minute and second "2018-01-02T03:04Z", # Missing mandatory minute and second "2018-01-02T03:04:01+04", # Missing mandatory offset minute "2018-01-02T03:04:05", # Missing mandatory offset "2018-01-02T03:04:05.12345", # Missing mandatory offset "2018-01-02T24:00:00Z", # 24:00:00 is not valid in RFC 3339 '20180102T03:04:05-12:34', # Missing mandatory date separators '2018-01-02T030405-12:34', # Missing mandatory time separators '2018-01-02T03:04:05-1234', # Missing mandatory offset separator '2018-01-02T03:04:05,12345Z' # Invalid comma fractional second separator ]: with self.assertRaisesRegex(ValueError, r"RFC 3339", msg="Timestamp '{0}' was supposed to be invalid, but parsing it didn't raise ValueError.".format(timestamp)): ciso8601.parse_rfc3339(timestamp) class GithubIssueRegressionTestCase(unittest.TestCase): # These are test cases that were provided in GitHub issues submitted to ciso8601. # They are kept here as regression tests. # They might not have any additional value above-and-beyond what is already tested in the normal unit tests. def test_issue_5(self): self.assertRaisesRegex( ValueError, r"Invalid character while parsing minute \(':', Index: 14\)", ciso8601.parse_datetime, '2014-02-03T10::27', ) def test_issue_6(self): self.assertRaisesRegex( ValueError, r"Invalid character while parsing second \('.', Index: 17\)", ciso8601.parse_datetime, '2014-02-03 04:05:.123456', ) def test_issue_8(self): self.assertRaisesRegex( ValueError, r"hour must be in 0..23", ciso8601.parse_datetime, '2001-01-01T24:01:01', ) self.assertRaisesRegex( ValueError, r"month must be in 1..12", ciso8601.parse_datetime, '07722968', ) def test_issue_13(self): self.assertRaisesRegex( ValueError, r"month must be in 1..12", ciso8601.parse_datetime, '2014-13-01', ) def test_issue_22(self): self.assertRaisesRegex( ValueError, r"day is out of range for month", ciso8601.parse_datetime, '2016-11-31T12:34:34.521059', ) def test_issue_35(self): self.assertRaisesRegex( ValueError, r"Invalid character while parsing date separator \('-'\) \('1', Index: 7\)", ciso8601.parse_datetime, '2017-0012-27T13:35:19+0200', ) def test_issue_42(self): self.assertRaisesRegex( ValueError, r"day is out of range for month", ciso8601.parse_datetime, '20140200', ) def test_issue_71(self): self.assertRaisesRegex( ValueError, r"Cannot combine \"basic\" date format with \"extended\" time format", ciso8601.parse_datetime, '20010203T04:05:06Z', ) self.assertRaisesRegex( ValueError, r"Cannot combine \"basic\" date format with \"extended\" time format", ciso8601.parse_datetime, '20010203T04:05', ) if __name__ == '__main__': unittest.main()