looker-sdk 24.18.0__py3-none-any.whl → 24.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- looker_sdk/rtl/__init__.py +22 -0
- looker_sdk/rtl/api_methods.py +247 -0
- looker_sdk/rtl/api_settings.py +194 -0
- looker_sdk/rtl/auth_session.py +353 -0
- looker_sdk/rtl/auth_token.py +101 -0
- looker_sdk/rtl/constants.py +31 -0
- looker_sdk/rtl/hooks.py +86 -0
- looker_sdk/rtl/model.py +230 -0
- looker_sdk/rtl/requests_transport.py +110 -0
- looker_sdk/rtl/serialize.py +120 -0
- looker_sdk/rtl/transport.py +137 -0
- looker_sdk/sdk/__init__.py +0 -0
- looker_sdk/sdk/api40/__init__.py +1 -0
- looker_sdk/sdk/api40/methods.py +13356 -0
- looker_sdk/sdk/api40/models.py +15616 -0
- looker_sdk/sdk/constants.py +24 -0
- looker_sdk/version.py +1 -1
- {looker_sdk-24.18.0.dist-info → looker_sdk-24.20.0.dist-info}/METADATA +1 -1
- looker_sdk-24.20.0.dist-info/RECORD +36 -0
- {looker_sdk-24.18.0.dist-info → looker_sdk-24.20.0.dist-info}/top_level.txt +1 -0
- tests/integration/__init__.py +2 -0
- tests/integration/test_methods.py +681 -0
- tests/integration/test_netrc.py +55 -0
- tests/rtl/__init__.py +2 -0
- tests/rtl/test_api_methods.py +216 -0
- tests/rtl/test_api_settings.py +252 -0
- tests/rtl/test_auth_session.py +284 -0
- tests/rtl/test_auth_token.py +70 -0
- tests/rtl/test_requests_transport.py +171 -0
- tests/rtl/test_serialize.py +770 -0
- tests/rtl/test_transport.py +34 -0
- looker_sdk-24.18.0.dist-info/RECORD +0 -9
- {looker_sdk-24.18.0.dist-info → looker_sdk-24.20.0.dist-info}/LICENSE.txt +0 -0
- {looker_sdk-24.18.0.dist-info → looker_sdk-24.20.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
# The MIT License (MIT)
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2019 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
|
+
|
|
23
|
+
import copy
|
|
24
|
+
import datetime
|
|
25
|
+
import enum
|
|
26
|
+
import functools
|
|
27
|
+
import json
|
|
28
|
+
|
|
29
|
+
from typing import Optional, Sequence
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from typing import ForwardRef # type: ignore
|
|
33
|
+
except ImportError:
|
|
34
|
+
from typing import _ForwardRef as ForwardRef # type: ignore
|
|
35
|
+
|
|
36
|
+
import attr
|
|
37
|
+
import cattr
|
|
38
|
+
import pytest # type: ignore
|
|
39
|
+
|
|
40
|
+
from looker_sdk.rtl import hooks
|
|
41
|
+
from looker_sdk.rtl import model as ml
|
|
42
|
+
from looker_sdk.rtl import serialize as sr
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Enum1(enum.Enum):
|
|
46
|
+
"""Predifined enum, used as ForwardRef."""
|
|
47
|
+
|
|
48
|
+
entry1 = "entry1"
|
|
49
|
+
entry2 = "entry2"
|
|
50
|
+
invalid_api_enum_value = "invalid_api_enum_value"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ignore mypy: "Cannot assign to a method"
|
|
54
|
+
Enum1.__new__ = ml.safe_enum__new__ # type: ignore
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@attr.s(auto_attribs=True, init=False)
|
|
58
|
+
class ModelNoRefs1(ml.Model):
|
|
59
|
+
"""Predifined model, used as ForwardRef.
|
|
60
|
+
|
|
61
|
+
Since this model has no properties that are forwardrefs to other
|
|
62
|
+
objects we can just decorate the class rather than doing the
|
|
63
|
+
__annotations__ hack.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
name1: str
|
|
67
|
+
|
|
68
|
+
def __init__(self, *, name1: str):
|
|
69
|
+
self.name1 = name1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@attr.s(auto_attribs=True, init=False)
|
|
73
|
+
class Model(ml.Model):
|
|
74
|
+
"""Representative model.
|
|
75
|
+
|
|
76
|
+
[De]Serialization of API models relies on the attrs and cattrs
|
|
77
|
+
libraries with some additional customization. This model represents
|
|
78
|
+
these custom treatments and provides documentation for how and why
|
|
79
|
+
they are needed.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# enum1 and model_no_refs1 are both defined before this class
|
|
83
|
+
# yet we will still refer to them using forward reference (double quotes)
|
|
84
|
+
# because we do not keep track of definition order in the generated code
|
|
85
|
+
enum1: "Enum1"
|
|
86
|
+
model_no_refs1: "ModelNoRefs1"
|
|
87
|
+
|
|
88
|
+
# enum2 and model_no_refs2 are both defined after this class and so the
|
|
89
|
+
# forward reference annotation is required.
|
|
90
|
+
enum2: "Enum2"
|
|
91
|
+
model_no_refs2: "ModelNoRefs2"
|
|
92
|
+
|
|
93
|
+
# Optional[] and List[]
|
|
94
|
+
list_enum1: Sequence["Enum1"]
|
|
95
|
+
list_model_no_refs1: Sequence["ModelNoRefs1"]
|
|
96
|
+
opt_enum1: Optional["Enum1"] = None
|
|
97
|
+
opt_model_no_refs1: Optional["ModelNoRefs1"] = None
|
|
98
|
+
list_opt_model_no_refs1: Optional[Sequence["ModelNoRefs1"]] = None
|
|
99
|
+
|
|
100
|
+
# standard types
|
|
101
|
+
id: Optional[int] = None
|
|
102
|
+
name: Optional[str] = None
|
|
103
|
+
datetime_field: Optional[datetime.datetime] = None
|
|
104
|
+
|
|
105
|
+
# testing reserved keyword translations
|
|
106
|
+
class_: Optional[str] = None
|
|
107
|
+
finally_: Optional[Sequence[int]] = None
|
|
108
|
+
|
|
109
|
+
# Because this model has "bare" forward ref annotated properties
|
|
110
|
+
# (enum1, enum2, model_no_refs1, and model_no_refs2) we need to tell
|
|
111
|
+
# the attr library that they're actually ForwardRef objects so that
|
|
112
|
+
# cattr will match our forward_ref_structure_hook structure hook
|
|
113
|
+
#
|
|
114
|
+
#
|
|
115
|
+
# Note: just doing the following:
|
|
116
|
+
#
|
|
117
|
+
# `converter.register_structure_hook("Enum1", structure_hook)`
|
|
118
|
+
#
|
|
119
|
+
# does not work. cattr stores these hooks using functools singledispatch
|
|
120
|
+
# which in turn creates a weakref.WeakKeyDictionary for the dispatch_cache.
|
|
121
|
+
# While the registration happens, the cache lookup throws a TypeError
|
|
122
|
+
# instead of a KeyError so we never look in the registry.
|
|
123
|
+
__annotations__ = {
|
|
124
|
+
# python generates these entries as "enum1": "Enum1" etc, we need
|
|
125
|
+
# them to be of the form "enum1": ForwardRef("Enum1")
|
|
126
|
+
"enum1": ForwardRef("Enum1"),
|
|
127
|
+
"model_no_refs1": ForwardRef("ModelNoRefs1"),
|
|
128
|
+
"enum2": ForwardRef("Enum2"),
|
|
129
|
+
"model_no_refs2": ForwardRef("ModelNoRefs2"),
|
|
130
|
+
# python "correctly" inserts the remaining entries but we have to
|
|
131
|
+
# define all or nothing using this API
|
|
132
|
+
"list_enum1": Sequence["Enum1"],
|
|
133
|
+
"list_model_no_refs1": Sequence["ModelNoRefs1"],
|
|
134
|
+
"opt_enum1": Optional["Enum1"],
|
|
135
|
+
"opt_model_no_refs1": Optional["ModelNoRefs1"],
|
|
136
|
+
"list_opt_model_no_refs1": Optional[Sequence["ModelNoRefs1"]],
|
|
137
|
+
"id": Optional[int],
|
|
138
|
+
"name": Optional[str],
|
|
139
|
+
"datetime_field": Optional[datetime.datetime],
|
|
140
|
+
"class_": Optional[str],
|
|
141
|
+
"finally_": Optional[Sequence[int]],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
# store context so that base class can eval "Enum1" instance from
|
|
145
|
+
# ForwardRef("Enum1") annotation for __setitem__
|
|
146
|
+
__global_context = globals()
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
enum1: "Enum1",
|
|
152
|
+
model_no_refs1: "ModelNoRefs1",
|
|
153
|
+
enum2: "Enum2",
|
|
154
|
+
model_no_refs2: "ModelNoRefs2",
|
|
155
|
+
list_enum1: Sequence["Enum1"],
|
|
156
|
+
list_model_no_refs1: Sequence["ModelNoRefs1"],
|
|
157
|
+
opt_enum1: Optional["Enum1"] = None,
|
|
158
|
+
opt_model_no_refs1: Optional["ModelNoRefs1"] = None,
|
|
159
|
+
list_opt_model_no_refs1: Optional[Sequence["ModelNoRefs1"]] = None,
|
|
160
|
+
id: Optional[int] = None,
|
|
161
|
+
name: Optional[str] = None,
|
|
162
|
+
datetime_field: Optional[datetime.datetime] = None,
|
|
163
|
+
class_: Optional[str] = None,
|
|
164
|
+
finally_: Optional[Sequence[int]] = None,
|
|
165
|
+
):
|
|
166
|
+
"""Keep mypy and IDE suggestions happy.
|
|
167
|
+
|
|
168
|
+
We cannot use the built in __init__ generation attrs offers
|
|
169
|
+
because mypy complains about unknown keyword argument, even
|
|
170
|
+
when kw_only=True is set below. Furthermore, IDEs do not pickup
|
|
171
|
+
on the attrs generated __init__ so completion suggestion fails
|
|
172
|
+
for insantiating these classes otherwise.
|
|
173
|
+
"""
|
|
174
|
+
self.enum1 = enum1
|
|
175
|
+
self.model_no_refs1 = model_no_refs1
|
|
176
|
+
self.enum2 = enum2
|
|
177
|
+
self.model_no_refs2 = model_no_refs2
|
|
178
|
+
self.list_enum1 = list_enum1
|
|
179
|
+
self.list_model_no_refs1 = list_model_no_refs1
|
|
180
|
+
self.opt_enum1 = opt_enum1
|
|
181
|
+
self.opt_model_no_refs1 = opt_model_no_refs1
|
|
182
|
+
self.list_opt_model_no_refs1 = list_opt_model_no_refs1
|
|
183
|
+
self.id = id
|
|
184
|
+
self.name = name
|
|
185
|
+
self.datetime_field = datetime_field
|
|
186
|
+
self.class_ = class_
|
|
187
|
+
self.finally_ = finally_
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class Enum2(enum.Enum):
|
|
191
|
+
"""Post defined enum, used as ForwardRef."""
|
|
192
|
+
|
|
193
|
+
entry2 = "entry2"
|
|
194
|
+
invalid_api_enum_value = "invalid_api_enum_value"
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ignore mypy: "Cannot assign to a method"
|
|
198
|
+
Enum2.__new__ = ml.safe_enum__new__ # type: ignore
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@attr.s(auto_attribs=True, init=False)
|
|
202
|
+
class ModelNoRefs2(ml.Model):
|
|
203
|
+
"""Post defined model, used as ForwardRef.
|
|
204
|
+
|
|
205
|
+
Since this model has no properties that are forwardrefs to other
|
|
206
|
+
objects we can just decorate the class rather than doing the
|
|
207
|
+
__annotations__ hack.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
name2: str
|
|
211
|
+
|
|
212
|
+
def __init__(self, *, name2: str):
|
|
213
|
+
self.name2 = name2
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
converter = cattr.Converter()
|
|
217
|
+
translate_keys_structure_hook = functools.partial(
|
|
218
|
+
sr.translate_keys_structure_hook, converter
|
|
219
|
+
)
|
|
220
|
+
converter.register_structure_hook(Model, translate_keys_structure_hook)
|
|
221
|
+
converter.register_structure_hook(datetime.datetime, hooks.datetime_structure_hook)
|
|
222
|
+
converter.register_unstructure_hook(datetime.datetime, hooks.datetime_unstructure_hook)
|
|
223
|
+
unstructure_hook = functools.partial(hooks.unstructure_hook, converter)
|
|
224
|
+
converter.register_unstructure_hook(Model, unstructure_hook)
|
|
225
|
+
|
|
226
|
+
# only required for 3.6 for unittest but for some reason integration tests need it
|
|
227
|
+
# for all python versions
|
|
228
|
+
forward_ref_structure_hook = functools.partial(
|
|
229
|
+
sr.forward_ref_structure_hook, globals(), converter
|
|
230
|
+
)
|
|
231
|
+
converter.register_structure_hook_func(
|
|
232
|
+
lambda t: t.__class__ is ForwardRef, forward_ref_structure_hook
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
DATETIME_VALUE = datetime.datetime.fromtimestamp(1625246159, datetime.timezone.utc)
|
|
237
|
+
DATETIME_VALUE_STR = DATETIME_VALUE.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
|
|
238
|
+
MODEL_DATA = {
|
|
239
|
+
"enum1": "entry1",
|
|
240
|
+
"model_no_refs1": {"name1": "model_no_refs1_name"},
|
|
241
|
+
"enum2": "entry2",
|
|
242
|
+
"model_no_refs2": {"name2": "model_no_refs2_name"},
|
|
243
|
+
"list_enum1": ["entry1"],
|
|
244
|
+
"list_model_no_refs1": [{"name1": "model_no_refs1_name"}],
|
|
245
|
+
"opt_enum1": "entry1",
|
|
246
|
+
"opt_model_no_refs1": {"name1": "model_no_refs1_name"},
|
|
247
|
+
"list_opt_model_no_refs1": [{"name1": "model_no_refs1_name"}],
|
|
248
|
+
"id": 1,
|
|
249
|
+
"name": "my-name",
|
|
250
|
+
"datetime_field": DATETIME_VALUE_STR,
|
|
251
|
+
"class": "model-name",
|
|
252
|
+
"finally": [1, 2, 3],
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@pytest.fixture
|
|
257
|
+
def bm():
|
|
258
|
+
return Model(
|
|
259
|
+
enum1=Enum1.entry1,
|
|
260
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
261
|
+
enum2=Enum2.entry2,
|
|
262
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
263
|
+
list_enum1=[Enum1.entry1],
|
|
264
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
265
|
+
opt_enum1=Enum1.entry1,
|
|
266
|
+
opt_model_no_refs1=None,
|
|
267
|
+
id=1,
|
|
268
|
+
name="my-name",
|
|
269
|
+
datetime_field=DATETIME_VALUE,
|
|
270
|
+
class_="model-name",
|
|
271
|
+
finally_=[1, 2, 3],
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_dict_getitem_simple(bm):
|
|
276
|
+
assert bm["id"] == bm.id
|
|
277
|
+
assert bm["name"] == bm.name
|
|
278
|
+
assert bm["class"] == bm.class_
|
|
279
|
+
assert bm["finally"] == bm.finally_
|
|
280
|
+
with pytest.raises(KeyError):
|
|
281
|
+
bm["class_"]
|
|
282
|
+
with pytest.raises(KeyError):
|
|
283
|
+
bm["finally_"]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_dict_getitem_child(bm):
|
|
287
|
+
assert bm["model_no_refs1"] == ModelNoRefs1(name1="model_no_refs1_name")
|
|
288
|
+
assert bm["model_no_refs1"] is bm.model_no_refs1
|
|
289
|
+
assert bm["model_no_refs1"]["name1"] == "model_no_refs1_name"
|
|
290
|
+
assert bm["list_model_no_refs1"][0] == ModelNoRefs1(name1="model_no_refs1_name")
|
|
291
|
+
assert bm["list_model_no_refs1"][0] is bm.list_model_no_refs1[0]
|
|
292
|
+
assert bm["list_model_no_refs1"][0]["name1"] == "model_no_refs1_name"
|
|
293
|
+
# Model defines this property and `bm.opt_model_no_refs1 is None` so key
|
|
294
|
+
# access here should do the same (https://git.io/JRrKm)
|
|
295
|
+
assert bm["opt_model_no_refs1"] is None
|
|
296
|
+
with pytest.raises(KeyError):
|
|
297
|
+
bm["no_such_prop"]
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_dict_getitem_enum(bm):
|
|
301
|
+
assert bm["enum1"] == "entry1"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_dict_setitem_simple(bm):
|
|
305
|
+
bm["id"] = 2
|
|
306
|
+
assert bm["id"] == 2
|
|
307
|
+
assert bm.id == 2
|
|
308
|
+
bm["name"] = "some-name"
|
|
309
|
+
assert bm["name"] == "some-name"
|
|
310
|
+
assert bm.name == "some-name"
|
|
311
|
+
bm["class"] = "some-class"
|
|
312
|
+
assert bm["class"] == "some-class"
|
|
313
|
+
assert bm.class_ == "some-class"
|
|
314
|
+
bm["finally"] = [4, 5, 6]
|
|
315
|
+
assert bm["finally"] == [4, 5, 6]
|
|
316
|
+
assert bm["finally"] is bm.finally_
|
|
317
|
+
with pytest.raises(AttributeError) as exc:
|
|
318
|
+
bm["foobar"] = 5
|
|
319
|
+
assert str(exc.value) == "'Model' object has no attribute 'foobar'"
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_dict_setitem_child(bm):
|
|
323
|
+
bm["model_no_refs1"]["name1"] = "model_no_refs1_another_name"
|
|
324
|
+
assert bm["model_no_refs1"]["name1"] == "model_no_refs1_another_name"
|
|
325
|
+
|
|
326
|
+
# creating new children from dictionaries instantiates a new child model
|
|
327
|
+
# object from that dict but does not maintain a reference.
|
|
328
|
+
child_dict = {"name1": "I used a dictionary"}
|
|
329
|
+
bm["model_no_refs1"] = child_dict
|
|
330
|
+
assert bm["model_no_refs1"] == ModelNoRefs1(name1="I used a dictionary")
|
|
331
|
+
assert bm.model_no_refs1 is not child_dict
|
|
332
|
+
bm["model_no_refs1"]["name1"] = "I'm not a reference to child_dict"
|
|
333
|
+
assert child_dict["name1"] != "I'm not a reference to child_dict"
|
|
334
|
+
assert child_dict["name1"] == "I used a dictionary"
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def test_dict_setitem_enum(bm):
|
|
338
|
+
bm["enum1"] = "entry2"
|
|
339
|
+
assert bm["enum1"] == "entry2"
|
|
340
|
+
# it's really still an Enum1 member under the hood
|
|
341
|
+
assert bm.enum1 == Enum1.entry2
|
|
342
|
+
with pytest.raises(ValueError) as exc:
|
|
343
|
+
bm["enum1"] = "foobar"
|
|
344
|
+
assert str(exc.value) == (
|
|
345
|
+
"Invalid value 'foobar' for 'Model.enum1'. Valid values are ['entry1', 'entry2']"
|
|
346
|
+
)
|
|
347
|
+
with pytest.raises(ValueError) as exc:
|
|
348
|
+
bm["enum1"] = Enum1.entry1 # can't use a real Enum with dict
|
|
349
|
+
assert str(exc.value) == (
|
|
350
|
+
"Invalid value 'Enum1.entry1' for 'Model.enum1'. Valid values are "
|
|
351
|
+
"['entry1', 'entry2']"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_dict_delitem(bm):
|
|
356
|
+
del bm["id"]
|
|
357
|
+
assert bm.id is None
|
|
358
|
+
# Model defines this property and `bm.id is None` so key
|
|
359
|
+
# access here should do the same (https://git.io/JRrKm)
|
|
360
|
+
assert bm["id"] is None
|
|
361
|
+
del bm["class"]
|
|
362
|
+
assert bm.class_ is None
|
|
363
|
+
# Model defines this property and `bm.class_ is None` so key
|
|
364
|
+
# access here should do the same (https://git.io/JRrKm)
|
|
365
|
+
assert bm["class"] is None
|
|
366
|
+
with pytest.raises(KeyError):
|
|
367
|
+
del bm["no-such-key"]
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_dict_iter(bm):
|
|
371
|
+
keys = [
|
|
372
|
+
"enum1",
|
|
373
|
+
"model_no_refs1",
|
|
374
|
+
"enum2",
|
|
375
|
+
"model_no_refs2",
|
|
376
|
+
"list_enum1",
|
|
377
|
+
"list_model_no_refs1",
|
|
378
|
+
"opt_enum1",
|
|
379
|
+
"id",
|
|
380
|
+
"name",
|
|
381
|
+
"datetime_field",
|
|
382
|
+
"class",
|
|
383
|
+
"finally",
|
|
384
|
+
]
|
|
385
|
+
for k in bm:
|
|
386
|
+
keys.remove(k)
|
|
387
|
+
assert keys == []
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_dict_contains(bm):
|
|
391
|
+
assert "id" in bm
|
|
392
|
+
assert "foobar" not in bm
|
|
393
|
+
assert "finally_" not in bm
|
|
394
|
+
assert "finally" in bm
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_dict_keys(bm):
|
|
398
|
+
assert list(bm.keys()) == [
|
|
399
|
+
"enum1",
|
|
400
|
+
"model_no_refs1",
|
|
401
|
+
"enum2",
|
|
402
|
+
"model_no_refs2",
|
|
403
|
+
"list_enum1",
|
|
404
|
+
"list_model_no_refs1",
|
|
405
|
+
"opt_enum1",
|
|
406
|
+
"id",
|
|
407
|
+
"name",
|
|
408
|
+
"datetime_field",
|
|
409
|
+
"class",
|
|
410
|
+
"finally",
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def test_dict_items(bm):
|
|
415
|
+
assert list(bm.items()) == [
|
|
416
|
+
("enum1", "entry1"),
|
|
417
|
+
("model_no_refs1", {"name1": "model_no_refs1_name"}),
|
|
418
|
+
("enum2", "entry2"),
|
|
419
|
+
("model_no_refs2", {"name2": "model_no_refs2_name"}),
|
|
420
|
+
("list_enum1", ["entry1"]),
|
|
421
|
+
("list_model_no_refs1", [{"name1": "model_no_refs1_name"}]),
|
|
422
|
+
("opt_enum1", "entry1"),
|
|
423
|
+
("id", 1),
|
|
424
|
+
("name", "my-name"),
|
|
425
|
+
("datetime_field", DATETIME_VALUE_STR),
|
|
426
|
+
("class", "model-name"),
|
|
427
|
+
("finally", [1, 2, 3]),
|
|
428
|
+
]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def test_dict_values(bm):
|
|
432
|
+
assert list(bm.values()) == [
|
|
433
|
+
"entry1",
|
|
434
|
+
{"name1": "model_no_refs1_name"},
|
|
435
|
+
"entry2",
|
|
436
|
+
{"name2": "model_no_refs2_name"},
|
|
437
|
+
["entry1"],
|
|
438
|
+
[{"name1": "model_no_refs1_name"}],
|
|
439
|
+
"entry1",
|
|
440
|
+
1,
|
|
441
|
+
"my-name",
|
|
442
|
+
DATETIME_VALUE_STR,
|
|
443
|
+
"model-name",
|
|
444
|
+
[1, 2, 3],
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_dict_get(bm):
|
|
449
|
+
assert bm.get("id") == bm.id
|
|
450
|
+
assert bm.get("id", 5000) == bm.id
|
|
451
|
+
assert bm.get("not-a-key") is None
|
|
452
|
+
assert bm.get("not-a-key", "default-value") == "default-value"
|
|
453
|
+
assert bm.get("model_no_refs1") is bm["model_no_refs1"]
|
|
454
|
+
assert bm["model_no_refs1"].get("name1") == "model_no_refs1_name"
|
|
455
|
+
assert bm["model_no_refs1"].get("name1", "default-name") == "model_no_refs1_name"
|
|
456
|
+
assert bm["model_no_refs1"].get("name2") is None
|
|
457
|
+
assert bm["model_no_refs1"].get("name2", "default-name") == "default-name"
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def test_dict_pop(bm):
|
|
461
|
+
assert bm.pop("id") == 1
|
|
462
|
+
assert bm.id is None
|
|
463
|
+
# Model defines this property and `bm.id is None` so key
|
|
464
|
+
# access here should do the same (https://git.io/JRrKm)
|
|
465
|
+
assert bm["id"] is None
|
|
466
|
+
|
|
467
|
+
assert bm.pop("name", "default-name") == "my-name"
|
|
468
|
+
assert bm.name is None
|
|
469
|
+
# Model defines this property and `bm.name is None` so key
|
|
470
|
+
# access here should do the same (https://git.io/JRrKm)
|
|
471
|
+
assert bm["name"] is None
|
|
472
|
+
|
|
473
|
+
assert bm.pop("no-name", "default-name") == "default-name"
|
|
474
|
+
|
|
475
|
+
assert bm.pop("class") == "model-name"
|
|
476
|
+
assert bm.class_ is None
|
|
477
|
+
# Model defines this property and `bm.name is None` so key
|
|
478
|
+
# access here should do the same (https://git.io/JRrKm)
|
|
479
|
+
assert bm["class"] is None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def test_dict_popitem(bm):
|
|
483
|
+
with pytest.raises(NotImplementedError):
|
|
484
|
+
bm.popitem()
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_dict_clear(bm):
|
|
488
|
+
with pytest.raises(NotImplementedError):
|
|
489
|
+
bm.popitem()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_dict_update(bm):
|
|
493
|
+
bm.update(
|
|
494
|
+
{
|
|
495
|
+
"id": 2,
|
|
496
|
+
"name": "new-name",
|
|
497
|
+
"class": "new-class",
|
|
498
|
+
"model_no_refs1": {"name1": "new-name1"},
|
|
499
|
+
}
|
|
500
|
+
)
|
|
501
|
+
assert bm["id"] == 2
|
|
502
|
+
assert bm["name"] == "new-name"
|
|
503
|
+
assert bm["class"] == "new-class"
|
|
504
|
+
assert bm["model_no_refs1"]["name1"] == "new-name1"
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_dict_setdefault(bm):
|
|
508
|
+
bm.setdefault("id", 2)
|
|
509
|
+
assert bm["id"] == 1
|
|
510
|
+
del bm["id"]
|
|
511
|
+
bm.setdefault("id", 2)
|
|
512
|
+
assert bm["id"] == 2
|
|
513
|
+
|
|
514
|
+
with pytest.raises(AttributeError):
|
|
515
|
+
bm.setdefault("foobar", 5)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def test_dict_copy(bm):
|
|
519
|
+
with pytest.raises(NotImplementedError):
|
|
520
|
+
bm.copy()
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def test_deserialize_single() -> None:
|
|
524
|
+
"""Deserialize functionality
|
|
525
|
+
|
|
526
|
+
Should handle python reserved keywords as well as attempting to
|
|
527
|
+
convert field values to proper type.
|
|
528
|
+
"""
|
|
529
|
+
# check that type conversion happens, str -> int and int -> str in this case
|
|
530
|
+
data = copy.deepcopy(MODEL_DATA)
|
|
531
|
+
data["id"] = "1"
|
|
532
|
+
data["name"] = 25
|
|
533
|
+
|
|
534
|
+
d = json.dumps(data)
|
|
535
|
+
model = sr.deserialize(data=d, structure=Model, converter=converter)
|
|
536
|
+
assert model == Model(
|
|
537
|
+
enum1=Enum1.entry1,
|
|
538
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
539
|
+
enum2=Enum2.entry2,
|
|
540
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
541
|
+
list_enum1=[Enum1.entry1],
|
|
542
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
543
|
+
opt_enum1=Enum1.entry1,
|
|
544
|
+
opt_model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
545
|
+
list_opt_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
546
|
+
id=1,
|
|
547
|
+
name="25",
|
|
548
|
+
datetime_field=DATETIME_VALUE,
|
|
549
|
+
class_="model-name",
|
|
550
|
+
finally_=[1, 2, 3],
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def test_deserialize_list():
|
|
555
|
+
# check that type conversion happens
|
|
556
|
+
data = [MODEL_DATA]
|
|
557
|
+
|
|
558
|
+
models = sr.deserialize(
|
|
559
|
+
data=json.dumps(data), structure=Sequence[Model], converter=converter
|
|
560
|
+
)
|
|
561
|
+
assert models == [
|
|
562
|
+
Model(
|
|
563
|
+
enum1=Enum1.entry1,
|
|
564
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
565
|
+
enum2=Enum2.entry2,
|
|
566
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
567
|
+
list_enum1=[Enum1.entry1],
|
|
568
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
569
|
+
opt_enum1=Enum1.entry1,
|
|
570
|
+
opt_model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
571
|
+
list_opt_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
572
|
+
id=1,
|
|
573
|
+
name="my-name",
|
|
574
|
+
datetime_field=DATETIME_VALUE,
|
|
575
|
+
class_="model-name",
|
|
576
|
+
finally_=[1, 2, 3],
|
|
577
|
+
),
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def test_deserialize_partial():
|
|
582
|
+
data = copy.deepcopy(MODEL_DATA)
|
|
583
|
+
del data["id"]
|
|
584
|
+
del data["opt_enum1"]
|
|
585
|
+
del data["opt_model_no_refs1"]
|
|
586
|
+
|
|
587
|
+
model = sr.deserialize(data=json.dumps(data), structure=Model, converter=converter)
|
|
588
|
+
assert model == Model(
|
|
589
|
+
enum1=Enum1.entry1,
|
|
590
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
591
|
+
enum2=Enum2.entry2,
|
|
592
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
593
|
+
list_enum1=[Enum1.entry1],
|
|
594
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
595
|
+
opt_enum1=None,
|
|
596
|
+
opt_model_no_refs1=None,
|
|
597
|
+
list_opt_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
598
|
+
id=None,
|
|
599
|
+
name="my-name",
|
|
600
|
+
datetime_field=DATETIME_VALUE,
|
|
601
|
+
class_="model-name",
|
|
602
|
+
finally_=[1, 2, 3],
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def test_deserialize_with_null():
|
|
607
|
+
data = copy.deepcopy(MODEL_DATA)
|
|
608
|
+
|
|
609
|
+
# json.dumps sets None to null
|
|
610
|
+
data["id"] = None
|
|
611
|
+
data["opt_enum1"] = None
|
|
612
|
+
data["opt_model_no_refs1"] = None
|
|
613
|
+
|
|
614
|
+
model = sr.deserialize(data=json.dumps(data), structure=Model, converter=converter)
|
|
615
|
+
assert model == Model(
|
|
616
|
+
enum1=Enum1.entry1,
|
|
617
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
618
|
+
enum2=Enum2.entry2,
|
|
619
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
620
|
+
list_enum1=[Enum1.entry1],
|
|
621
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
622
|
+
opt_enum1=None,
|
|
623
|
+
opt_model_no_refs1=None,
|
|
624
|
+
list_opt_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
625
|
+
id=None,
|
|
626
|
+
name="my-name",
|
|
627
|
+
datetime_field=DATETIME_VALUE,
|
|
628
|
+
class_="model-name",
|
|
629
|
+
finally_=[1, 2, 3],
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@pytest.mark.skip(reason="TODO: This breaks CI right now")
|
|
634
|
+
@pytest.mark.parametrize(
|
|
635
|
+
"data, structure",
|
|
636
|
+
[
|
|
637
|
+
# ??
|
|
638
|
+
# Error: mypy: Variable "tests.rtl.test_serialize.Model" is not valid as a type
|
|
639
|
+
(MODEL_DATA, Sequence[Model]), # type: ignore
|
|
640
|
+
([MODEL_DATA], Model),
|
|
641
|
+
],
|
|
642
|
+
)
|
|
643
|
+
def test_deserialize_data_structure_mismatch(data, structure):
|
|
644
|
+
data = json.dumps(data)
|
|
645
|
+
with pytest.raises(sr.DeserializeError):
|
|
646
|
+
sr.deserialize(data=data, structure=structure, converter=converter)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def test_serialize_single():
|
|
650
|
+
model = Model(
|
|
651
|
+
enum1=Enum1.entry1,
|
|
652
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
653
|
+
enum2=Enum2.entry2,
|
|
654
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
655
|
+
list_enum1=[Enum1.entry1],
|
|
656
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
657
|
+
opt_enum1=Enum1.entry1,
|
|
658
|
+
opt_model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
659
|
+
list_opt_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
660
|
+
id=1,
|
|
661
|
+
name="my-name",
|
|
662
|
+
datetime_field=DATETIME_VALUE,
|
|
663
|
+
class_="model-name",
|
|
664
|
+
finally_=[1, 2, 3],
|
|
665
|
+
)
|
|
666
|
+
expected = json.dumps(MODEL_DATA).encode("utf-8")
|
|
667
|
+
assert sr.serialize(api_model=model, converter=converter) == expected
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def test_serialize_sequence():
|
|
671
|
+
model = Model(
|
|
672
|
+
enum1=Enum1.entry1,
|
|
673
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
674
|
+
enum2=Enum2.entry2,
|
|
675
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
676
|
+
list_enum1=[Enum1.entry1],
|
|
677
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
678
|
+
opt_enum1=Enum1.entry1,
|
|
679
|
+
opt_model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
680
|
+
list_opt_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
681
|
+
id=1,
|
|
682
|
+
name="my-name",
|
|
683
|
+
datetime_field=DATETIME_VALUE,
|
|
684
|
+
class_="model-name",
|
|
685
|
+
finally_=[1, 2, 3],
|
|
686
|
+
)
|
|
687
|
+
expected = json.dumps([MODEL_DATA, MODEL_DATA]).encode("utf-8")
|
|
688
|
+
assert sr.serialize(api_model=[model, model], converter=converter) == expected
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def test_serialize_partial():
|
|
692
|
+
"""Do not send json null for model None field values."""
|
|
693
|
+
model = Model(
|
|
694
|
+
enum1=Enum1.entry1,
|
|
695
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
696
|
+
enum2=Enum2.entry2,
|
|
697
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
698
|
+
list_enum1=[Enum1.entry1],
|
|
699
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
700
|
+
)
|
|
701
|
+
expected = json.dumps(
|
|
702
|
+
{
|
|
703
|
+
"enum1": "entry1",
|
|
704
|
+
"model_no_refs1": {"name1": "model_no_refs1_name"},
|
|
705
|
+
"enum2": "entry2",
|
|
706
|
+
"model_no_refs2": {"name2": "model_no_refs2_name"},
|
|
707
|
+
"list_enum1": ["entry1"],
|
|
708
|
+
"list_model_no_refs1": [{"name1": "model_no_refs1_name"}],
|
|
709
|
+
}
|
|
710
|
+
).encode("utf-8")
|
|
711
|
+
assert sr.serialize(api_model=model, converter=converter) == expected
|
|
712
|
+
|
|
713
|
+
@pytest.mark.skip(reason="TODO: This breaks CI right now")
|
|
714
|
+
def test_serialize_explict_null():
|
|
715
|
+
"""Send json null for model field EXPLICIT_NULL values."""
|
|
716
|
+
# pass EXPLICIT_NULL into constructor
|
|
717
|
+
model = Model(
|
|
718
|
+
enum1=Enum1.entry1,
|
|
719
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
720
|
+
enum2=Enum2.entry2,
|
|
721
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
722
|
+
list_enum1=[Enum1.entry1],
|
|
723
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
724
|
+
name=ml.EXPLICIT_NULL,
|
|
725
|
+
class_=ml.EXPLICIT_NULL,
|
|
726
|
+
)
|
|
727
|
+
# set property to EXPLICIT_NULL
|
|
728
|
+
model.finally_ = ml.EXPLICIT_NULL
|
|
729
|
+
|
|
730
|
+
expected = json.dumps(
|
|
731
|
+
{
|
|
732
|
+
"enum1": "entry1",
|
|
733
|
+
"model_no_refs1": {"name1": "model_no_refs1_name"},
|
|
734
|
+
"enum2": "entry2",
|
|
735
|
+
"model_no_refs2": {"name2": "model_no_refs2_name"},
|
|
736
|
+
"list_enum1": ["entry1"],
|
|
737
|
+
"list_model_no_refs1": [{"name1": "model_no_refs1_name"}],
|
|
738
|
+
# json.dumps puts these into the json as null
|
|
739
|
+
"name": None,
|
|
740
|
+
"class": None,
|
|
741
|
+
"finally": None,
|
|
742
|
+
}
|
|
743
|
+
).encode("utf-8")
|
|
744
|
+
assert sr.serialize(api_model=model, converter=converter) == expected
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def test_safe_enum_deserialization():
|
|
748
|
+
data = copy.deepcopy(MODEL_DATA)
|
|
749
|
+
data["enum1"] = "not an Enum1 member!"
|
|
750
|
+
data["enum2"] = ""
|
|
751
|
+
model = Model(
|
|
752
|
+
enum1=Enum1.invalid_api_enum_value,
|
|
753
|
+
model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
754
|
+
enum2=Enum2.invalid_api_enum_value,
|
|
755
|
+
model_no_refs2=ModelNoRefs2(name2="model_no_refs2_name"),
|
|
756
|
+
list_enum1=[Enum1.entry1],
|
|
757
|
+
list_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
758
|
+
opt_enum1=Enum1.entry1,
|
|
759
|
+
opt_model_no_refs1=ModelNoRefs1(name1="model_no_refs1_name"),
|
|
760
|
+
list_opt_model_no_refs1=[ModelNoRefs1(name1="model_no_refs1_name")],
|
|
761
|
+
id=1,
|
|
762
|
+
name="my-name",
|
|
763
|
+
datetime_field=DATETIME_VALUE,
|
|
764
|
+
class_="model-name",
|
|
765
|
+
finally_=[1, 2, 3],
|
|
766
|
+
)
|
|
767
|
+
assert (
|
|
768
|
+
sr.deserialize(data=json.dumps(data), structure=Model, converter=converter)
|
|
769
|
+
== model
|
|
770
|
+
)
|