1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
|
Patterned Date and Time
=======================
.. note::
**Important:** The current patterned date and time functionality is being phased out. Please refer to the new docs for **V1 Opt-in** features, which introduces enhanced support for patterned date-time strings. For more details, see the `Field Guide to V1 Opt‐in`_ and the `V1 Patterned Date and Time`_ documentation.
This change is part of the ongoing improvements in version ``v0.35.0+``, and the old functionality will no longer be maintained in future releases.
.. _Field Guide to V1 Opt‐in: https://github.com/rnag/dataclass-wizard/wiki/Field-Guide-to-V1-Opt%E2%80%90in
.. _V1 Patterned Date and Time: https://dataclass-wizard.readthedocs.io/en/latest/common_use_cases/v1_patterned_date_time.html
Loading an `ISO 8601`_ format string into a :class:`date` / :class:`time` /
:class:`datetime` object is already handled as part of the de-serialization
process by default. For example, a date string in ISO format such as
``2022-01-17T21:52:18.000Z`` is correctly parsed to :class:`datetime` as expected.
However, what happens when you have a date string in |another format|_, such
as ``November 2, 2021``, and you want to load it to a :class:`date`
or :class:`datetime` object?
As of *v0.20.0*, the accepted solution is to use the builtin support for
parsing strings with custom date-time patterns; this internally calls
:meth:`datetime.strptime` to match input strings against a specified pattern.
There are two approaches (shown below) that can be used to specify custom patterns
for date-time strings. The simplest approach is to annotate fields as either
a :class:`DatePattern`, :class:`TimePattern`, or a :class:`DateTimePattern`.
.. note::
The input date-time strings are parsed in the following sequence:
- In case it's an `ISO 8601`_ format string, or a numeric timestamp,
we attempt to parse with the default load function such as
:func:`as_datetime`. Note that we initially parse strings using the
builtin :meth:`fromisoformat` method, as this is `much faster`_ than
using :meth:`datetime.strptime`. If the date string is matched, we
immediately return the new date-time object.
- Next, we parse with :meth:`datetime.strptime` by passing in the
*pattern* to match against. If the pattern is invalid, a
``ParseError`` is raised at this stage.
In any case, the :class:`date`, :class:`time`, and :class:`datetime` objects
are dumped (serialized) as `ISO 8601`_ format strings, which is the default
behavior. As we initially attempt to parse with :meth:`fromisoformat` in the
load (de-serialization) process as mentioned, it turns out
`much faster`_ to load any data that has been previously serialized in
ISO-8601 format.
The usage is shown below, and is again pretty straightforward.
.. code:: python3
from dataclasses import dataclass
from datetime import datetime
from typing import Annotated
from dataclass_wizard import JSONWizard, Pattern, DatePattern, TimePattern
@dataclass
class MyClass(JSONWizard):
# 1 -- Annotate with `DatePattern`, `TimePattern`, or `DateTimePattern`.
# Upon de-serialization, the underlying types will be `date`,
# `time`, and `datetime` respectively.
date_field: DatePattern['%b %d, %Y']
time_field: TimePattern['%I:%M %p']
# 2 -- Use `Annotated` to annotate the field as `list[time]` for example,
# and pass in `Pattern` as an extra.
dt_field: Annotated[datetime, Pattern('%m/%d/%y %H:%M:%S')]
data = {'date_field': 'Jan 3, 2022',
'time_field': '3:45 PM',
'dt_field': '01/02/23 02:03:52'}
# Deserialize the data into a `MyClass` object
c1 = MyClass.from_dict(data)
print('Deserialized object:', repr(c1))
# MyClass(date_field=datetime.date(2022, 1, 3),
# time_field=datetime.time(15, 45),
# dt_field=datetime.datetime(2023, 1, 2, 2, 3, 52))
# Print the prettified JSON representation. Note that date/times are
# converted to ISO 8601 format here.
print(c1)
# {
# "dateField": "2022-01-03",
# "timeField": "15:45:00",
# "dtField": "2023-01-02T02:03:52"
# }
# Confirm that we can load the serialized data as expected.
c2 = MyClass.from_json(c1.to_json())
# Assert that the data is the same
assert c1 == c2
Containers of Date and Time
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Suppose the type annotation for a dataclass field is more complex -- for example,
an annotation might be a ``list[date]`` instead, representing an ordered
collection of :class:`date` objects.
In such cases, you can use ``Annotated`` along with :func:`Pattern`, as shown
below. Note that this also allows you to more easily annotate using a subtype
of date-time, for example a subclass of :class:`date` if so desired.
.. code:: python3
from dataclasses import dataclass
from datetime import datetime, time
from typing import Annotated
from dataclass_wizard import JSONWizard, Pattern
class MyTime(time):
"""A custom `time` subclass"""
def get_hour(self):
return self.hour
@dataclass
class MyClass(JSONWizard):
time_field: Annotated[list[MyTime], Pattern('%I:%M %p')]
dt_mapping: Annotated[dict[int, datetime], Pattern('%b.%d.%y %H,%M,%S')]
data = {'time_field': ['3:45 PM', '1:20 am', '12:30 pm'],
'dt_mapping': {'1133': 'Jan.2.20 15,20,57',
'5577': 'Nov.27.23 2,52,11'},
}
# Deserialize the data into a `MyClass` object
c1 = MyClass.from_dict(data)
print('Deserialized object:', repr(c1))
# MyClass(time_field=[MyTime(15, 45), MyTime(1, 20), MyTime(12, 30)],
# dt_mapping={1133: datetime.datetime(2020, 1, 2, 15, 20, 57),
# 5577: datetime.datetime(2023, 11, 27, 2, 52, 11)})
# Print the prettified JSON representation. Note that date/times are
# converted to ISO 8601 format here.
print(c1)
# {
# "timeField": [
# "15:45:00",
# "01:20:00",
# "12:30:00"
# ],
# "dtMapping": {
# "1133": "2020-01-02T15:20:57",
# "5577": "2023-11-27T02:52:11"
# }
# }
# Confirm that we can load the serialized data as expected.
c2 = MyClass.from_json(c1.to_json())
# Assert that the data is the same
assert c1 == c2
.. _ISO 8601: https://en.wikipedia.org/wiki/ISO_8601
.. _much faster: https://stackoverflow.com/questions/13468126/a-faster-strptime
.. See: https://stackoverflow.com/a/4836544/10237506
.. |another format| replace:: *another* format
.. _another format: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
|