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

add parsing of pendulum_dt from unix time #185

Merged
merged 1 commit into from
Jun 11, 2024
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
38 changes: 27 additions & 11 deletions pydantic_extra_types/pendulum_dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@
from pydantic_core import PydanticCustomError, core_schema


class DateTime(_DateTime):
class DateTimeSettings(type):
def __new__(cls, name, bases, dct, **kwargs): # type: ignore[no-untyped-def]
dct['strict'] = kwargs.pop('strict', True)
return super().__new__(cls, name, bases, dct)

def __init__(cls, name, bases, dct, **kwargs): # type: ignore[no-untyped-def]
super().__init__(name, bases, dct)
cls.strict = kwargs.get('strict', True)


class DateTime(_DateTime, metaclass=DateTimeSettings):
"""
A `pendulum.DateTime` object. At runtime, this type decomposes into pendulum.DateTime automatically.
This type exists because Pydantic throws a fit on unknown types.
Expand Down Expand Up @@ -54,7 +64,7 @@ def __get_pydantic_core_schema__(cls, source: Type[Any], handler: GetCoreSchemaH
return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.datetime_schema())

@classmethod
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'DateTime':
"""
Validate the datetime object and return it.

Expand All @@ -68,12 +78,10 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
# if we are passed an existing instance, pass it straight through.
if isinstance(value, (_DateTime, datetime)):
return DateTime.instance(value)

# otherwise, parse it.
try:
value = parse(value, exact=True)
if not isinstance(value, _DateTime):
raise ValueError(f'value is not a valid datetime it is a {type(value)}')
# probably the best way to have feature parity with
# https://docs.pydantic.dev/latest/api/standard_library_types/#datetimedatetime
value = handler(value)
return DateTime(
value.year,
value.month,
Expand All @@ -84,8 +92,16 @@ def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler
value.microsecond,
value.tzinfo,
)
except Exception as exc:
raise PydanticCustomError('value_error', 'value is not a valid timestamp') from exc
except ValueError:
try:
value = parse(value, strict=cls.strict)
if isinstance(value, _DateTime):
return DateTime.instance(value)
raise ValueError(f'value is not a valid datetime it is a {type(value)}')
except ValueError:
raise
except Exception as exc:
raise PydanticCustomError('value_error', 'value is not a valid datetime') from exc


class Date(_Date):
Expand Down Expand Up @@ -123,7 +139,7 @@ def __get_pydantic_core_schema__(cls, source: Type[Any], handler: GetCoreSchemaH
return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.date_schema())

@classmethod
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'Date':
"""
Validate the date object and return it.

Expand Down Expand Up @@ -183,7 +199,7 @@ def __get_pydantic_core_schema__(cls, source: Type[Any], handler: GetCoreSchemaH
return core_schema.no_info_wrap_validator_function(cls._validate, core_schema.timedelta_schema())

@classmethod
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> Any:
def _validate(cls, value: Any, handler: core_schema.ValidatorFunctionWrapHandler) -> 'Duration':
"""
Validate the Duration object and return it.

Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ filterwarnings = [
'error',
# This ignore will be removed when pycountry will drop py36 & support py311
'ignore:::pkg_resources',
# This ignore will be removed when pendulum fixes https://github.com/sdispater/pendulum/issues/834
'ignore:datetime.datetime.utcfromtimestamp.*:DeprecationWarning'
]

# configuring https://github.com/pydantic/hooky
Expand Down
151 changes: 146 additions & 5 deletions tests/test_pendulum_dt.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,21 @@

UTC = tz.utc

DtTypeAdapter = TypeAdapter(datetime)


class DtModel(BaseModel):
dt: DateTime


class DateTimeNonStrict(DateTime, strict=False):
pass


class DtModelNotStrict(BaseModel):
dt: DateTimeNonStrict


class DateModel(BaseModel):
d: Date

Expand Down Expand Up @@ -119,6 +129,91 @@ def test_pendulum_dt_from_serialized(dt):
assert isinstance(model.dt, pendulum.DateTime)


@pytest.mark.parametrize(
'dt',
[
pendulum.now().to_iso8601_string(),
pendulum.now().to_w3c_string(),
'Sat Oct 11 17:13:46 UTC 2003', # date util parsing
pendulum.now().to_iso8601_string()[:5], # actualy valid or pendulum.parse(dt, strict=False) would fail here
],
)
def test_pendulum_dt_not_strict_from_serialized(dt):
"""
Verifies that building an instance from serialized, well-formed strings decode properly.
"""
dt_actual = pendulum.parse(dt, strict=False)
model = DtModelNotStrict(dt=dt)
assert model.dt == dt_actual
assert type(model.dt) is DateTime
assert isinstance(model.dt, pendulum.DateTime)


@pytest.mark.parametrize(
'dt',
[
pendulum.now().to_iso8601_string(),
pendulum.now().to_w3c_string(),
1718096578,
1718096578.5,
-5,
-5.5,
float('-0'),
'1718096578',
'1718096578.5',
'-5',
'-5.5',
'-0',
'-0.0',
'+0.0',
'+1718096578.5',
float('-2e10') - 1.0,
float('2e10') + 1.0,
-2e10 - 1,
2e10 + 1,
],
)
def test_pendulum_dt_from_str_unix_timestamp(dt):
"""
Verifies that building an instance from serialized, well-formed strings decode properly.
"""
dt_actual = pendulum.instance(DtTypeAdapter.validate_python(dt))
model = DtModel(dt=dt)
assert model.dt == dt_actual
assert type(model.dt) is DateTime
assert isinstance(model.dt, pendulum.DateTime)


@pytest.mark.parametrize(
'dt',
[
1718096578,
1718096578.5,
-5,
-5.5,
float('-0'),
'1718096578',
'1718096578.5',
'-5',
'-5.5',
'-0',
'-0.0',
'+0.0',
'+1718096578.5',
float('-2e10') - 1.0,
float('2e10') + 1.0,
-2e10 - 1,
2e10 + 1,
],
)
def test_pendulum_dt_from_str_unix_timestamp_is_utc(dt):
"""
Verifies that without timezone information, it is coerced to UTC. As in pendulum
"""
model = DtModel(dt=dt)
assert model.dt.tzinfo.tzname(model.dt) == 'UTC'


@pytest.mark.parametrize(
'd',
[pendulum.now().date().isoformat(), pendulum.now().to_w3c_string(), pendulum.now().to_iso8601_string()],
Expand Down Expand Up @@ -155,22 +250,68 @@ def test_pendulum_duration_from_serialized(delta_t_str):
assert isinstance(model.delta_t, pendulum.Duration)


@pytest.mark.parametrize('dt', [None, 'malformed', pendulum.now().to_iso8601_string()[:5], 42, 'P10Y10M10D'])
def get_invalid_dt_common():
return [
None,
'malformed',
'P10Y10M10D',
float('inf'),
float('-inf'),
'inf',
'-inf',
'INF',
'-INF',
'+inf',
'Infinity',
'+Infinity',
'-Infinity',
'INFINITY',
'+INFINITY',
'-INFINITY',
'infinity',
'+infinity',
'-infinity',
float('nan'),
'nan',
'NaN',
'NAN',
'+nan',
'-nan',
]


dt_strict = get_invalid_dt_common()
dt_strict.append(pendulum.now().to_iso8601_string()[:5])


@pytest.mark.parametrize(
'dt',
dt_strict,
)
def test_pendulum_dt_malformed(dt):
"""
Verifies that the instance fails to validate if malformed dt are passed.
Verifies that the instance fails to validate if malformed dt is passed.
"""
with pytest.raises(ValidationError):
DtModel(dt=dt)


@pytest.mark.parametrize('date', [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 42, 'P10Y10M10D'])
def test_pendulum_date_malformed(date):
@pytest.mark.parametrize('dt', get_invalid_dt_common())
def test_pendulum_dt_non_strict_malformed(dt):
"""
Verifies that the instance fails to validate if malformed dt are passed.
"""
with pytest.raises(ValidationError):
DtModelNotStrict(dt=dt)


@pytest.mark.parametrize('invalid_value', [None, 'malformed', pendulum.today().to_iso8601_string()[:5], 'P10Y10M10D'])
def test_pendulum_date_malformed(invalid_value):
"""
Verifies that the instance fails to validate if malformed date are passed.
"""
with pytest.raises(ValidationError):
DateModel(d=date)
DateModel(d=invalid_value)


@pytest.mark.parametrize(
Expand Down
Loading