-
-
Notifications
You must be signed in to change notification settings - Fork 690
Add timestamp as option to DateTime Fields #2022
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
Changes from 2 commits
20fa305
1bd06f8
baa2a2d
60804d1
648da8a
529f166
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1218,25 +1218,32 @@ class DateTime(Field): | |
| Example: ``'2014-12-22T03:12:58.019077+00:00'`` | ||
|
|
||
| :param format: Either ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601), | ||
| or a date format string. If `None`, defaults to "iso". | ||
| ``"timestamp"``, ``"timestamp_ms"`` (for a POSIX timestamp) or a date format string. | ||
| If `None`, defaults to "iso". | ||
| :param kwargs: The same keyword arguments that :class:`Field` receives. | ||
|
|
||
| .. versionchanged:: 3.0.0rc9 | ||
| Does not modify timezone information on (de)serialization. | ||
| .. versionchanged:: 3.18 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Slightly outdated.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to 3.19 ✨ |
||
| Add timestamp as a format. | ||
| """ | ||
|
|
||
| SERIALIZATION_FUNCS = { | ||
| "iso": utils.isoformat, | ||
| "iso8601": utils.isoformat, | ||
| "rfc": utils.rfcformat, | ||
| "rfc822": utils.rfcformat, | ||
| } # type: typing.Dict[str, typing.Callable[[typing.Any], str]] | ||
| "timestamp": utils.timestamp, | ||
| "timestamp_ms": utils.timestamp_ms, | ||
| } # type: typing.Dict[str, typing.Callable[[typing.Any], str | float]] | ||
|
|
||
| DESERIALIZATION_FUNCS = { | ||
| "iso": utils.from_iso_datetime, | ||
| "iso8601": utils.from_iso_datetime, | ||
| "rfc": utils.from_rfc, | ||
| "rfc822": utils.from_rfc, | ||
| "timestamp": utils.from_timestamp, | ||
| "timestamp_ms": utils.from_timestamp_ms, | ||
| } # type: typing.Dict[str, typing.Callable[[str], typing.Any]] | ||
|
|
||
| DEFAULT_FORMAT = "iso" | ||
|
|
@@ -1252,7 +1259,7 @@ class DateTime(Field): | |
| "format": '"{input}" cannot be formatted as a {obj_type}.', | ||
| } | ||
|
|
||
| def __init__(self, format: str | None = None, **kwargs): | ||
| def __init__(self, format: str | None = None, **kwargs) -> None: | ||
| super().__init__(**kwargs) | ||
| # Allow this to be None. It may be set later in the ``_serialize`` | ||
| # or ``_deserialize`` methods. This allows a Schema to dynamically set the | ||
|
|
@@ -1267,7 +1274,7 @@ def _bind_to_schema(self, field_name, schema): | |
| or self.DEFAULT_FORMAT | ||
| ) | ||
|
|
||
| def _serialize(self, value, attr, obj, **kwargs): | ||
| def _serialize(self, value, attr, obj, **kwargs) -> str | float | None: | ||
| if value is None: | ||
| return None | ||
| data_format = self.format or self.DEFAULT_FORMAT | ||
|
|
@@ -1277,7 +1284,7 @@ def _serialize(self, value, attr, obj, **kwargs): | |
| else: | ||
| return value.strftime(data_format) | ||
|
|
||
| def _deserialize(self, value, attr, data, **kwargs): | ||
| def _deserialize(self, value, attr, data, **kwargs) -> dt.datetime: | ||
| if not value: # Falsy values, e.g. '', None, [] are not valid | ||
| raise self.make_error("invalid", input=value, obj_type=self.OBJ_TYPE) | ||
| data_format = self.format or self.DEFAULT_FORMAT | ||
|
|
@@ -1298,7 +1305,7 @@ def _deserialize(self, value, attr, data, **kwargs): | |
| ) from error | ||
|
|
||
| @staticmethod | ||
| def _make_object_from_format(value, data_format): | ||
| def _make_object_from_format(value, data_format) -> dt.datetime: | ||
| return dt.datetime.strptime(value, data_format) | ||
|
|
||
|
|
||
|
|
@@ -1323,11 +1330,11 @@ def __init__( | |
| *, | ||
| timezone: dt.timezone | None = None, | ||
| **kwargs, | ||
| ): | ||
| ) -> None: | ||
| super().__init__(format=format, **kwargs) | ||
| self.timezone = timezone | ||
|
|
||
| def _deserialize(self, value, attr, data, **kwargs): | ||
| def _deserialize(self, value, attr, data, **kwargs) -> dt.datetime: | ||
| ret = super()._deserialize(value, attr, data, **kwargs) | ||
| if is_aware(ret): | ||
| if self.timezone is None: | ||
|
|
@@ -1360,11 +1367,11 @@ def __init__( | |
| *, | ||
| default_timezone: dt.tzinfo | None = None, | ||
| **kwargs, | ||
| ): | ||
| ) -> None: | ||
| super().__init__(format=format, **kwargs) | ||
| self.default_timezone = default_timezone | ||
|
|
||
| def _deserialize(self, value, attr, data, **kwargs): | ||
| def _deserialize(self, value, attr, data, **kwargs) -> dt.datetime: | ||
| ret = super()._deserialize(value, attr, data, **kwargs) | ||
| if not is_aware(ret): | ||
| if self.default_timezone is None: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -190,6 +190,36 @@ def from_iso_date(value): | |
| return dt.date(**kw) | ||
|
|
||
|
|
||
| def from_timestamp(value: typing.Any) -> dt.datetime: | ||
| value = float(value) | ||
| if value < 0: | ||
| raise ValueError("Not a valid POSIX timestamp") | ||
|
|
||
| # Load a timestamp with utc as timezone to prevent using system timezone. | ||
| # Then set timezone to None, to let the Field handle adding timezone info. | ||
| return dt.datetime.fromtimestamp(value, tz=dt.timezone.utc).replace(tzinfo=None) | ||
|
|
||
|
|
||
| def from_timestamp_ms(value: typing.Any) -> dt.datetime: | ||
| value = float(value) | ||
| return from_timestamp(value / 1000) | ||
|
|
||
|
|
||
| def timestamp( | ||
| value: dt.datetime, | ||
| ) -> float: | ||
| if is_aware(value): | ||
| return value.timestamp() | ||
|
|
||
| # When a date is naive, use utc as zone info to prevent using system timezone. | ||
| # See Python docs for more info: https://docs.python.org/3.10/library/datetime.html#datetime.datetime.timestamp | ||
| return value.replace(tzinfo=dt.timezone.utc).timestamp() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd do it the other way around. if not is_aware(value):
# When a date is naive, use UTC as zone info to prevent using system timezone.
value = value.replace(tzinfo=dt.timezone.utc)
return value.timestamp()
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree this is a bit cleaner ✔️ |
||
|
|
||
|
|
||
| def timestamp_ms(value: dt.datetime) -> float: | ||
| return timestamp(value) * 1000 | ||
|
|
||
|
|
||
| def isoformat(datetime: dt.datetime) -> str: | ||
| """Return the ISO8601-formatted representation of a datetime object. | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering (out loud) about the choices and their naming. I don't use timestamps. Are those two the two common uses?
timestampis a number of seconds. Should we call ittimestamp_s? I don't think so. Why the need fortimestamp_ms(and not other units)? Is it also commonly used?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review! Good question. These are the two common use cases (to my knowledge). JavaScript tends to use timestamps in milliseconds, while Python uses seconds by default.