1# The MIT License (MIT)
2#
3# Copyright (c) 2022 Looker Data Sciences, Inc.
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy
6# of this software and associated documentation files (the "Software"), to deal
7# in the Software without restriction, including without limitation the rights
8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9# copies of the Software, and to permit persons to whom the Software is
10# furnished to do so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in
13# all copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21# THE SOFTWARE.
22
23import datetime
24import enum
25import keyword
26import sys
27from typing import Type
28
29
30def unstructure_hook(converter, api_model):
31 """cattr unstructure hook
32
33 Map reserved_ words in models to correct json field names.
34 Also handle stripping None fields from dict while setting
35 EXPLICIT_NULL fields to None so that we only send null
36 in the json for fields the caller set EXPLICIT_NULL on.
37 """
38 data = converter.unstructure_attrs_asdict(api_model)
39 for key, value in data.copy().items():
40 if value is None:
41 del data[key]
42 elif value == "EXPLICIT_NULL":
43 data[key] = None
44 # bug here: in the unittests cattrs unstructures this correctly
45 # as an enum calling .value but in the integration tests we see
46 # it doesn't for WriteCreateQueryTask.result_format for some reason
47 # Haven't been able to debug it fully, so catching and processing
48 # it here.
49 elif isinstance(value, enum.Enum):
50 data[key] = value.value
51 for reserved in keyword.kwlist:
52 if f"{reserved}_" in data:
53 data[reserved] = data.pop(f"{reserved}_")
54 return data
55
56
57DATETIME_FMT = "%Y-%m-%dT%H:%M:%S.%f%z"
58if sys.version_info < (3, 7):
59 from dateutil import parser
60
61 def datetime_structure_hook(
62 d: str, t: Type[datetime.datetime]
63 ) -> datetime.datetime:
64 return parser.isoparse(d)
65
66else:
67
68 def datetime_structure_hook(
69 d: str, t: Type[datetime.datetime]
70 ) -> datetime.datetime:
71 return datetime.datetime.strptime(d, DATETIME_FMT)
72
73
74def datetime_unstructure_hook(dt):
75 return dt.strftime(DATETIME_FMT)
76
77
78def tr_data_keys(data):
79 """Map top level json keys to model property names.
80
81 Currently this translates reserved python keywords like "from" => "from_"
82 """
83 for reserved in keyword.kwlist:
84 if reserved in data and isinstance(data, dict):
85 data[f"{reserved}_"] = data.pop(reserved)
86 return data