File: patterned_date_time.rst

package info (click to toggle)
dataclass-wizard 0.35.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,808 kB
  • sloc: python: 15,276; makefile: 111; javascript: 23
file content (172 lines) | stat: -rw-r--r-- 6,805 bytes parent folder | download
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