Passed
Push — main ( 6d4817...ffd182 )
by Douglas
01:37
created

NestedDotDict.read_pickle()   A

Complexity

Conditions 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nop 2
dl 0
loc 10
rs 10
c 0
b 0
f 0
1
from __future__ import annotations
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
3
import pickle
4
import sys
5
from collections.abc import ByteString, Callable, Collection, Mapping, Sequence
6
from copy import copy
7
from datetime import date, datetime
8
from typing import Any, TypeVar
9
10
import orjson
0 ignored issues
show
introduced by
Unable to import 'orjson'
Loading history...
11
import tomlkit
0 ignored issues
show
introduced by
Unable to import 'tomlkit'
Loading history...
12
13
from pocketutils.core.exceptions import XKeyError, XTypeError, XValueError
0 ignored issues
show
Bug introduced by
The name core does not seem to exist in module pocketutils.
Loading history...
14
15
T = TypeVar("T")
0 ignored issues
show
Coding Style Naming introduced by
Class name "T" doesn't conform to PascalCase naming style ('[^\\W\\da-z][^\\W_]+$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
16
PICKLE_PROTOCOL = 5
17
18
19
def _json_encode_default(obj: Any) -> Any:
0 ignored issues
show
Unused Code introduced by
Either all return statements in a function should return an expression, or none of them should.
Loading history...
20
    if isinstance(obj, NestedDotDict):
21
        # noinspection PyProtectedMember
22
        return dict(obj._x)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _x was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
23
24
25
class NestedDotDict(Mapping):
0 ignored issues
show
best-practice introduced by
Too many public methods (22/20)
Loading history...
26
    """
27
    A thin wrapper around a nested dict to make getting values easier.
28
    This was designed as a wrapper for TOML, but it works more generally too.
29
30
    Keys must be strings that do not contain a dot (.).
31
    A dot is reserved for splitting values to traverse the tree.
32
    For example, ``dotdict["pet.species.name"]``.
33
    """
34
35
    @classmethod
36
    def parse_toml(cls, data: str) -> NestedDotDict:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
37
        return cls(tomlkit.loads(data))
38
39
    @classmethod
40
    def parse_json(cls, data: str) -> NestedDotDict:
41
        """
42
        Parses JSON from a string, into a NestedDotDict.
43
        If the JSON data is a list type, converts into a dict with the key ``data``.
44
        """
45
        data = orjson.loads(data.encode(encoding="utf-8"))
46
        if isinstance(data, list):
47
            data = dict(enumerate(data))
48
        return cls(data)
49
50
    @classmethod
51
    def parse_pickle(cls, data: ByteString) -> NestedDotDict:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
52
        if not isinstance(data, bytes):
53
            data = bytes(data)
54
        return NestedDotDict(pickle.loads(data))
55
56
    def to_pickle(self, protocol: int = PICKLE_PROTOCOL) -> bytes:
0 ignored issues
show
Unused Code introduced by
The argument protocol seems to be unused.
Loading history...
57
        """
58
        Writes to a pickle file.
59
        """
60
        return pickle.dumps(self._x, protocol=PICKLE_PROTOCOL)
61
62
    def __init__(self, x: Mapping[str, Any]) -> None:
0 ignored issues
show
introduced by
Value 'Mapping' is unsubscriptable
Loading history...
63
        """
64
        Constructor.
65
66
        Raises:
67
            XValueError: If a key (in this dict or a sub-dict) is not a str or contains a dot
68
        """
69
        if not (hasattr(x, "items") and hasattr(x, "keys") and hasattr(x, "values")):
70
            raise XTypeError(
71
                f"Type {type(x)} for value {x} appears not to be dict-like", actual=str(type(x))
72
            )
73
        bad = [k for k in x if not isinstance(k, str)]
74
        if len(bad) > 0:
75
            raise XValueError(f"Keys were not strings for these values: {bad}", value=bad)
76
        bad = [k for k in x if "." in k]
77
        if len(bad) > 0:
78
            raise XValueError(f"Keys contained dots (.) for these values: {bad}", value=bad)
79
        self._x = x
80
        # Let's make sure this constructor gets called on sub-dicts:
81
        self.leaves()
82
83
    def to_json(self, *, indent: bool = False) -> str:
84
        """
85
        Returns JSON text.
86
        """
87
        kwargs = dict(option=orjson.OPT_INDENT_2) if indent else {}
88
        encoded = orjson.dumps(self._x, default=_json_encode_default, **kwargs)
89
        return encoded.decode(encoding="utf-8")
90
91
    def to_toml(self) -> str:
92
        """
93
        Returns TOML text.
94
        """
95
        return tomlkit.dumps(self._x)
96
97
    def n_elements_total(self) -> int:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
98
        return len(self._all_elements())
99
100
    def n_bytes_total(self) -> int:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
101
        return sum([sys.getsizeof(x) for x in self._all_elements()])
102
103
    def _all_elements(self) -> Sequence[Any]:
0 ignored issues
show
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
104
        i = []
105
        for key, value in self._x.items():
0 ignored issues
show
Unused Code introduced by
The variable key seems to be unused.
Loading history...
106
            if value is not None and isinstance(value, Mapping):
107
                i += NestedDotDict(value)._all_elements()
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _all_elements was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
108
            elif (
109
                value is not None
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
110
                and isinstance(value, Collection)
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
111
                and not isinstance(value, str)
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
112
                and not isinstance(value, ByteString)
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
113
            ):
114
                i += list(value)
115
            else:
116
                i.append(value)
117
        return i
118
119
    def leaves(self) -> Mapping[str, Any]:
0 ignored issues
show
introduced by
Value 'Mapping' is unsubscriptable
Loading history...
120
        """
121
        Gets the leaves in this tree.
122
123
        Returns:
124
            A dict mapping dot-joined keys to their values
125
        """
126
        mp = {}
0 ignored issues
show
Coding Style Naming introduced by
Variable name "mp" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
127
        for key, value in self._x.items():
128
            if value is not None and isinstance(value, Mapping):
129
                mp.update({key + "." + k: v for k, v in NestedDotDict(value).leaves().items()})
130
            else:
131
                mp[key] = value
132
        return mp
133
134
    def sub(self, items: str) -> NestedDotDict:
135
        """
136
        Returns the dictionary under (dotted) keys ``items``.
137
138
        See Also:
139
            :meth:`sub_opt`
140
        """
141
        return NestedDotDict(self[items])
142
143
    def sub_opt(self, items: str) -> NestedDotDict:
144
        """
145
        Returns the dictionary under (dotted) keys ``items``, or empty if a key is not found.
146
147
        See Also:
148
            :meth:`sub`
149
        """
150
        try:
151
            return NestedDotDict(self[items])
152
        except XKeyError:
153
            return NestedDotDict({})
154
155
    def exactly(self, items: str, astype: type[T]) -> T:
0 ignored issues
show
introduced by
Value 'type' is unsubscriptable
Loading history...
156
        """
157
        Gets the key ``items`` from the dict if it has type ``astype``.
158
159
        Args:
160
            items: The key hierarchy, with a dot (.) as a separator
161
            astype: The type, which will be checked using ``isinstance``
162
163
        Returns:
164
            The value in the required type
165
166
        Raises:
167
            XTypeError: If not ``isinstance(value, astype)``
168
        """
169
        z = self[items]
0 ignored issues
show
Coding Style Naming introduced by
Variable name "z" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
170
        if not isinstance(z, astype):
171
            raise XTypeError(
172
                f"Value {z} from {items} is a {type(z)}, not {astype}",
173
                actual=str(type(z)),
174
                expected=str(astype),
175
            )
176
        return z
177
178
    def get_as(self, items: str, astype: Callable[[Any], T], default: T | None = None) -> T | None:
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
introduced by
Value 'Callable' is unsubscriptable
Loading history...
179
        """
180
        Gets the value of an *optional* key, or ``default`` if it doesn't exist.
181
        Calls ``astype(value)`` on the value before returning.
182
183
        See Also:
184
            :meth:`req_as`
185
            :meth:`exactly`
186
187
        Args:
188
            items: The key hierarchy, with a dot (.) as a separator.
189
                   Ex: ``animal.species.name``.
190
            astype: Any function that converts the found value to type ``T``.
191
                    Can be a ``Type``, such as ``int``.
192
                    Despite the annotated type, this function only needs to accept the actual value of the key
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (110/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
193
                    as input, not ``Any``.
194
            default: Return this value if the key is not found (at any level)
195
196
        Returns:
197
            The value of found key in this dot-dict, or ``default``.
198
199
        Raises:
200
            XValueError: Likely exception raised if calling ``astype`` fails
201
        """
202
        x = self.get(items)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
203
        if x is None:
204
            return default
205
        if astype is date:
206
            return self._to_date(x)
207
        if astype is datetime:
208
            return self._to_datetime(x)
209
        return astype(x)
210
211
    def req_as(self, items: str, astype: Callable[[Any], T] | None) -> T:
0 ignored issues
show
introduced by
Value 'Callable' is unsubscriptable
Loading history...
212
        """
213
        Gets the value of a *required* key.
214
        Calls ``astype(value)`` on the value before returning.
215
216
        See Also:
217
            :meth:`req_as`
218
            :meth:`exactly`
219
220
        Args:
221
            items: The key hierarchy, with a dot (.) as a separator.
222
                   Ex: ``animal.species.name``.
223
            astype: Any function that converts the found value to type ``T``.
224
                    Can be a ``Type``, such as ``int``.
225
                    Despite the annotated type, this function only needs to accept the actual value of the key
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (110/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
226
                    as input, not ``Any``.
227
228
        Returns:
229
            The value of found key in this dot-dict.
230
231
        Raises:
232
            XKeyError: If the key is not found (at any level).
233
            XValueError: Likely exception raised if calling ``astype`` fails
234
        """
235
        x = self[items]
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
236
        return astype(x)
237
238
    def get_list_as(
239
        self, items: str, astype: Callable[[Any], T], default: Sequence[T] | None = None
0 ignored issues
show
Coding Style introduced by
No space allowed around keyword argument assignment
Loading history...
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
introduced by
Value 'Callable' is unsubscriptable
Loading history...
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
240
    ) -> Sequence[T] | None:
0 ignored issues
show
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
241
        """
242
        Gets list values from an *optional* key.
243
        Note that ``astype`` here converts elements *within* the list, not the whole list.
244
        Also see ``req_list_as``.
245
246
        Args:
247
            items: The key hierarchy, with a dot (.) as a separator. Ex: ``animal.species.name``.
248
            astype: Any function that converts the found value to type ``T``. Ex: ``int``.
249
            default: Return this value if the key wasn't found
250
251
        Returns:
252
            ``[astype(v) for v in self[items]]``, or ``default`` if ``items`` was not found.
253
254
        Raises:
255
            XValueError: Likely exception raised if calling ``astype`` fails
256
            XTypeError: If the found value is not a (non-``str``) ``Sequence``
257
        """
258
        x = self.get(items)
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
259
        if x is None:
260
            return default
261
        if not isinstance(x, Sequence) or isinstance(x, str):
262
            raise XTypeError(f"Value {x} is not a list for lookup {items}", actual=str(type(x)))
263
        return [astype(y) for y in x]
264
265
    def req_list_as(self, items: str, astype: Callable[[Any], T] | None) -> Sequence[T]:
0 ignored issues
show
introduced by
Value 'Callable' is unsubscriptable
Loading history...
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
266
        """
267
        Gets list values from a *required* key.
268
        Note that ``astype`` here converts elements *within* the list, not the whole list.
269
        Also see ``get_list_as``.
270
271
        Args:
272
            items: The key hierarchy, with a dot (.) as a separator. Ex: ``animal.species.name``.
273
            astype: Any function that converts the found value to type ``T``. Ex: ``int``.
274
275
        Returns:
276
            ``[astype(v) for v in self[items]]``
277
278
        Raises:
279
            XValueError: Likely exception raised if calling ``astype`` fails
280
            XTypeError: If the found value is not a (non-``str``) ``Sequence``
281
            XKeyError: If the key was not found (at any level)
282
        """
283
        x = self[items]
0 ignored issues
show
Coding Style Naming introduced by
Variable name "x" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
284
        if not isinstance(x, Sequence) or isinstance(x, str):
285
            raise XTypeError(f"Value {x} is not a list for lookup {items}", actual=str(type(x)))
286
        return [astype(y) for y in x]
287
288
    def get(self, items: str, default: Any = None) -> Any:
0 ignored issues
show
Bug introduced by
Parameters differ from overridden 'get' method
Loading history...
289
        """
290
        Gets a value from an optional key.
291
        Also see ``__getitem__``.
292
        """
293
        try:
294
            return self[items]
295
        except KeyError:
296
            return default
297
298
    def __getitem__(self, items: str) -> Any:
299
        """
300
        Gets a value from a required key.
301
        Analogous to ``dict.__getitem__``, but this can operate on dot-joined strings.
302
303
        **NOTE:** The number of keys for which this returns a value can be different from ``len(self)``.
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (104/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
304
305
        Example:
306
            >>> d = NestedDotDict(dict(a=dict(b=1)))
307
            >>> assert d["a.b"] == 1
308
        """
309
        at = self._x
0 ignored issues
show
Coding Style Naming introduced by
Variable name "at" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
310
        for item in items.split("."):
311
            if item not in at:
312
                raise XKeyError(f"{items} not found: {item} does not exist")
313
            at = at[item]
0 ignored issues
show
Coding Style Naming introduced by
Variable name "at" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
314
        return NestedDotDict(at) if isinstance(at, dict) else copy(at)
315
316
    def items(self) -> Sequence[tuple[str, Any]]:
0 ignored issues
show
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
introduced by
Value 'tuple' is unsubscriptable
Loading history...
317
        return list(self._x.items())
318
319
    def keys(self) -> Sequence[str]:
0 ignored issues
show
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
320
        return list(self._x.keys())
321
322
    def values(self) -> Sequence[Any]:
0 ignored issues
show
introduced by
Value 'Sequence' is unsubscriptable
Loading history...
323
        return list(self._x.values())
324
325
    def pretty_str(self) -> str:
326
        """
327
        Pretty-prints the leaves of this dict using ``json.dumps``.
328
329
        Returns:
330
            A multi-line string
331
        """
332
        return orjson.dumps(
333
            self.leaves(), option=orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 | orjson.OPT_UTC_Z
334
        ).decode(encoding="utf-8")
335
336
    def __len__(self) -> int:
337
        """
338
        Returns the number of values in this dict.
339
        Does **NOT** include nested values.
340
        """
341
        return len(self._x)
342
343
    def is_empty(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
344
        return len(self._x) == 0
345
346
    def __iter__(self):
347
        """
348
        Iterates over values in this dict.
349
        Does **NOT** include nested items.
350
        """
351
        return iter(self._x)
352
353
    def __repr__(self):
354
        return repr(self._x)
355
356
    def __str__(self):
357
        return str(self._x)
358
359
    def __eq__(self, other):
360
        return str(self) == str(other)
361
362
    def _to_date(self, s) -> date:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "s" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
363
        if isinstance(s, date):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
364
            return s
365
        elif isinstance(s, str):
366
            # This is MUCH faster than tomlkit's
367
            return date.fromisoformat(s)
368
        else:
369
            raise XTypeError(f"Invalid type {type(s)} for {s}", actual=str(type(s)))
370
371
    def _to_datetime(self, s) -> datetime:
0 ignored issues
show
Coding Style Naming introduced by
Argument name "s" doesn't conform to snake_case naming style ('([^\\W\\dA-Z][^\\WA-Z]2,|_[^\\WA-Z]*|__[^\\WA-Z\\d_][^\\WA-Z]+__)$' pattern)

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
372
        if isinstance(s, datetime):
0 ignored issues
show
unused-code introduced by
Unnecessary "elif" after "return"
Loading history...
373
            return s
374
        elif isinstance(s, str):
375
            # This is MUCH faster than tomlkit's
376
            if s.count(":") < 2:
377
                raise XValueError(
378
                    f"Datetime {s} does not contain hours, minutes, and seconds", value=s
379
                )
380
            return datetime.fromisoformat(s.upper().replace("Z", "+00:00"))
381
        else:
382
            raise XTypeError(f"Invalid type {type(s)} for {s}", actual=str(type(s)))
383
384
385
__all__ = ["NestedDotDict"]
386