python-datamodel 0.6.28__cp313-cp313-win_amd64.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.
- datamodel/__init__.py +13 -0
- datamodel/abstract.py +189 -0
- datamodel/base.py +639 -0
- datamodel/converters.c +25347 -0
- datamodel/converters.cp313-win_amd64.pyd +0 -0
- datamodel/exceptions.c +13079 -0
- datamodel/exceptions.cp313-win_amd64.pyd +0 -0
- datamodel/fields.cp313-win_amd64.pyd +0 -0
- datamodel/fields.cpp +16655 -0
- datamodel/libs/__init__.py +0 -0
- datamodel/libs/mapping.c +14796 -0
- datamodel/libs/mapping.cp313-win_amd64.pyd +0 -0
- datamodel/libs/mutables.py +116 -0
- datamodel/models.py +111 -0
- datamodel/parsers/__init__.py +0 -0
- datamodel/parsers/encoders.py +15 -0
- datamodel/parsers/json.cp313-win_amd64.pyd +0 -0
- datamodel/parsers/json.cpp +12976 -0
- datamodel/profiler.py +21 -0
- datamodel/types.c +7137 -0
- datamodel/types.cp313-win_amd64.pyd +0 -0
- datamodel/validation.cp313-win_amd64.pyd +0 -0
- datamodel/validation.cpp +13275 -0
- datamodel/version.py +10 -0
- python_datamodel-0.6.28.dist-info/LICENSE +29 -0
- python_datamodel-0.6.28.dist-info/METADATA +316 -0
- python_datamodel-0.6.28.dist-info/RECORD +29 -0
- python_datamodel-0.6.28.dist-info/WHEEL +5 -0
- python_datamodel-0.6.28.dist-info/top_level.txt +1 -0
datamodel/base.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
# Dataclass
|
|
5
|
+
from dataclasses import (
|
|
6
|
+
_FIELD,
|
|
7
|
+
dataclass,
|
|
8
|
+
make_dataclass,
|
|
9
|
+
_MISSING_TYPE
|
|
10
|
+
)
|
|
11
|
+
from enum import EnumMeta
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
from orjson import OPT_INDENT_2
|
|
14
|
+
from .converters import parse_basic, parse_type, slugify_camelcase
|
|
15
|
+
from .fields import Field
|
|
16
|
+
from .types import JSON_TYPES, Text
|
|
17
|
+
from .validation import (
|
|
18
|
+
_validation,
|
|
19
|
+
is_callable,
|
|
20
|
+
is_empty,
|
|
21
|
+
is_dataclass,
|
|
22
|
+
is_primitive
|
|
23
|
+
)
|
|
24
|
+
from .exceptions import ValidationError
|
|
25
|
+
from .parsers.encoders import json_encoder
|
|
26
|
+
from .abstract import ModelMeta, Meta
|
|
27
|
+
from .models import ModelMixin
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _get_type_info(_type, name, title):
|
|
31
|
+
if _type.__module__ == 'typing':
|
|
32
|
+
if inspect.isfunction(_type):
|
|
33
|
+
if hasattr(_type, '__supertype__'):
|
|
34
|
+
return _type.__supertype__
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"You're using bare Functions to type hint on {name} for: {title}"
|
|
37
|
+
)
|
|
38
|
+
if _type._name == 'List':
|
|
39
|
+
return 'array'
|
|
40
|
+
if _type._name == 'Dict':
|
|
41
|
+
return 'object'
|
|
42
|
+
try:
|
|
43
|
+
return _type.__args__[0].__name__
|
|
44
|
+
except (AttributeError, ValueError):
|
|
45
|
+
return 'string'
|
|
46
|
+
elif hasattr(_type, '__supertype__'):
|
|
47
|
+
if type(_type) == type(Text):
|
|
48
|
+
return 'text'
|
|
49
|
+
if isinstance(_type.__supertype__, (str, int)):
|
|
50
|
+
return 'string' if isinstance(_type.__supertype__, str) else 'integer'
|
|
51
|
+
return JSON_TYPES.get(_type, 'string')
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_ref_info(_type, field):
|
|
55
|
+
if isinstance(_type, EnumMeta):
|
|
56
|
+
return {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"enum_type": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"enum": list(map(lambda c: c.value, _type))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
elif isinstance(_type, ModelMeta):
|
|
64
|
+
_schema = _type.schema(as_dict=True)
|
|
65
|
+
columns = []
|
|
66
|
+
if 'fk' not in field.metadata:
|
|
67
|
+
ref = _schema.get('$id', f"/{_type.__name__}")
|
|
68
|
+
else:
|
|
69
|
+
columns = field.metadata.get('fk').split("|")
|
|
70
|
+
_id, _value = columns
|
|
71
|
+
ref = {
|
|
72
|
+
"api": field.metadata.get('api', _schema['table']),
|
|
73
|
+
"id": _id,
|
|
74
|
+
"value": _value,
|
|
75
|
+
"$ref": _schema.get('$id', f"/{_type.__name__}")
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"schema": _schema,
|
|
80
|
+
"$ref": ref,
|
|
81
|
+
"columns": columns
|
|
82
|
+
}
|
|
83
|
+
elif 'api' in field.metadata:
|
|
84
|
+
# reference information, no matter the type:
|
|
85
|
+
try:
|
|
86
|
+
columns = field.metadata.get('fk').split("|")
|
|
87
|
+
_id, _value = columns
|
|
88
|
+
_fields = {
|
|
89
|
+
"id": _id,
|
|
90
|
+
"value": _value,
|
|
91
|
+
}
|
|
92
|
+
except (TypeError, ValueError):
|
|
93
|
+
_fields = {}
|
|
94
|
+
columns = []
|
|
95
|
+
ref = {
|
|
96
|
+
"api": field.metadata.get('api'),
|
|
97
|
+
**_fields
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"$ref": ref,
|
|
102
|
+
"columns": columns
|
|
103
|
+
}
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class BaseModel(ModelMixin, metaclass=ModelMeta):
|
|
108
|
+
"""
|
|
109
|
+
BaseModel.
|
|
110
|
+
Base Model for all DataModels.
|
|
111
|
+
"""
|
|
112
|
+
Meta = Meta
|
|
113
|
+
|
|
114
|
+
def __post_init__(self) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Post init method.
|
|
117
|
+
Fill fields with function-factory or calling validations
|
|
118
|
+
"""
|
|
119
|
+
# checking if an attribute is already a dataclass:
|
|
120
|
+
errors = {}
|
|
121
|
+
for name, f in self.__columns__.items():
|
|
122
|
+
try:
|
|
123
|
+
value = getattr(self, name)
|
|
124
|
+
if (error := self._process_field_(name, value, f)):
|
|
125
|
+
errors[name] = error
|
|
126
|
+
except RuntimeError as err:
|
|
127
|
+
logging.exception(err)
|
|
128
|
+
if errors:
|
|
129
|
+
if self.Meta.strict is True:
|
|
130
|
+
raise ValidationError(
|
|
131
|
+
f"""{self.modelName}: There are errors in Model. \
|
|
132
|
+
Hint: please check the "payload" attribute in the exception.""",
|
|
133
|
+
payload=errors
|
|
134
|
+
)
|
|
135
|
+
self.__errors__ = errors
|
|
136
|
+
object.__setattr__(self, "__valid__", False)
|
|
137
|
+
else:
|
|
138
|
+
object.__setattr__(self, "__valid__", True)
|
|
139
|
+
|
|
140
|
+
def _handle_default_value(self, value, f, name) -> Any:
|
|
141
|
+
# Calculate default value
|
|
142
|
+
if is_callable(value):
|
|
143
|
+
if value.__module__ != 'typing':
|
|
144
|
+
try:
|
|
145
|
+
new_val = value()
|
|
146
|
+
except TypeError:
|
|
147
|
+
try:
|
|
148
|
+
new_val = f.default()
|
|
149
|
+
except TypeError:
|
|
150
|
+
new_val = None
|
|
151
|
+
setattr(self, name, new_val)
|
|
152
|
+
elif is_callable(f.default) and value is None:
|
|
153
|
+
# Set the default value first
|
|
154
|
+
try:
|
|
155
|
+
new_val = f.default()
|
|
156
|
+
except (AttributeError, RuntimeError):
|
|
157
|
+
new_val = None
|
|
158
|
+
setattr(self, name, new_val)
|
|
159
|
+
value = new_val # Return the new value
|
|
160
|
+
elif not isinstance(f.default, _MISSING_TYPE) and value is None:
|
|
161
|
+
setattr(self, name, f.default)
|
|
162
|
+
value = f.default
|
|
163
|
+
return value
|
|
164
|
+
|
|
165
|
+
def _handle_dataclass_type(self, value, _type):
|
|
166
|
+
try:
|
|
167
|
+
if hasattr(self.Meta, 'no_nesting'):
|
|
168
|
+
return value
|
|
169
|
+
if value is None or is_dataclass(value):
|
|
170
|
+
return value
|
|
171
|
+
if isinstance(value, dict):
|
|
172
|
+
return _type(**value)
|
|
173
|
+
if isinstance(value, list):
|
|
174
|
+
return _type(*value)
|
|
175
|
+
return value if isinstance(value, (int, str, UUID)) else _type(value)
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"Invalid value for {_type}: {value}, error: {exc}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _handle_list_of_dataclasses(self, value, _type):
|
|
182
|
+
try:
|
|
183
|
+
sub_type = _type.__args__[0]
|
|
184
|
+
if is_dataclass(sub_type):
|
|
185
|
+
return [
|
|
186
|
+
sub_type(**item) if isinstance(item, dict) else item for item in value
|
|
187
|
+
]
|
|
188
|
+
except AttributeError:
|
|
189
|
+
pass
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
def _process_field_(
|
|
193
|
+
self,
|
|
194
|
+
name: str, value: Any, f: Field
|
|
195
|
+
) -> Optional[dict[Any, Any]]:
|
|
196
|
+
_type = f.type
|
|
197
|
+
_encoder = f.metadata.get('encoder')
|
|
198
|
+
new_val = value
|
|
199
|
+
if is_empty(value):
|
|
200
|
+
new_val = f.default_factory if isinstance(f.default, (_MISSING_TYPE)) else f.default
|
|
201
|
+
setattr(self, name, new_val)
|
|
202
|
+
|
|
203
|
+
if f.default is not None:
|
|
204
|
+
value = self._handle_default_value(value, f, name)
|
|
205
|
+
|
|
206
|
+
if is_primitive(_type):
|
|
207
|
+
try:
|
|
208
|
+
if value is not None:
|
|
209
|
+
new_val = parse_basic(f.type, value, _encoder)
|
|
210
|
+
return self._validation_(name, new_val, f, _type)
|
|
211
|
+
except (TypeError, ValueError) as ex:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"Wrong Type for {f.name}: {f.type}, error: {ex}"
|
|
214
|
+
) from ex
|
|
215
|
+
elif inspect.isclass(_type) and _type.__module__ == 'typing':
|
|
216
|
+
new_val = parse_type(_type, value, _encoder)
|
|
217
|
+
return self._validation_(name, new_val, f, _type)
|
|
218
|
+
elif isinstance(value, list) and hasattr(_type, '__args__'):
|
|
219
|
+
new_val = self._handle_list_of_dataclasses(value, _type)
|
|
220
|
+
return self._validation_(name, new_val, f, _type)
|
|
221
|
+
elif is_dataclass(_type):
|
|
222
|
+
new_val = self._handle_dataclass_type(value, _type)
|
|
223
|
+
return self._validation_(name, new_val, f, _type)
|
|
224
|
+
else:
|
|
225
|
+
try:
|
|
226
|
+
new_val = parse_type(f.type, value, _encoder)
|
|
227
|
+
except (TypeError, ValueError) as ex:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Wrong Type for {f.name}: {f.type}, error: {ex}"
|
|
230
|
+
) from ex
|
|
231
|
+
# Then validate the value
|
|
232
|
+
return self._validation_(name, new_val, f, _type)
|
|
233
|
+
|
|
234
|
+
def _field_checks_(self, f: Field, name: str, value: Any) -> None:
|
|
235
|
+
# Validate Primary Key
|
|
236
|
+
try:
|
|
237
|
+
if f.metadata['primary'] is True:
|
|
238
|
+
if 'db_default' in f.metadata:
|
|
239
|
+
pass
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"::{self.modelName}:: Missing Primary Key *{name}*"
|
|
243
|
+
)
|
|
244
|
+
except KeyError:
|
|
245
|
+
pass
|
|
246
|
+
# Validate Required
|
|
247
|
+
try:
|
|
248
|
+
if f.metadata["required"] is True and self.Meta.strict is True:
|
|
249
|
+
if 'db_default' in f.metadata:
|
|
250
|
+
return
|
|
251
|
+
if value is not None:
|
|
252
|
+
return # If default value is set, no need to raise an error
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"::{self.modelName}:: Missing Required Field *{name}*"
|
|
255
|
+
)
|
|
256
|
+
except KeyError:
|
|
257
|
+
return
|
|
258
|
+
# Nullable:
|
|
259
|
+
try:
|
|
260
|
+
if f.metadata["nullable"] is False and self.Meta.strict is True:
|
|
261
|
+
raise ValueError(
|
|
262
|
+
f"::{self.modelName}:: *{name}* Cannot be null."
|
|
263
|
+
)
|
|
264
|
+
except KeyError:
|
|
265
|
+
return
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
def _validation_(
|
|
269
|
+
self,
|
|
270
|
+
name: str,
|
|
271
|
+
value: Any,
|
|
272
|
+
f: Field, _type: Any
|
|
273
|
+
) -> Optional[dict[Any, Any]]:
|
|
274
|
+
"""
|
|
275
|
+
_validation_.
|
|
276
|
+
TODO: cover validations as length, not_null, required, max, min, etc
|
|
277
|
+
"""
|
|
278
|
+
val_type = type(value)
|
|
279
|
+
# Set the current Value
|
|
280
|
+
setattr(self, name, value)
|
|
281
|
+
|
|
282
|
+
if val_type == type or value == _type or is_empty(value):
|
|
283
|
+
try:
|
|
284
|
+
self._field_checks_(f, name, value)
|
|
285
|
+
return None
|
|
286
|
+
except (ValueError, TypeError):
|
|
287
|
+
raise
|
|
288
|
+
else:
|
|
289
|
+
# capturing other errors from validator:
|
|
290
|
+
return _validation(f, name, value, _type, val_type)
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def add_field(cls, name: str, value: Any = None) -> None:
|
|
294
|
+
if cls.Meta.strict is True:
|
|
295
|
+
raise TypeError(
|
|
296
|
+
f'Cannot create a new field {name} on a Strict Model.'
|
|
297
|
+
)
|
|
298
|
+
if name != '__errors__':
|
|
299
|
+
f = Field(required=False, default=value)
|
|
300
|
+
f.name = name
|
|
301
|
+
f.type = type(value)
|
|
302
|
+
f._field_type = _FIELD
|
|
303
|
+
cls.__columns__[name] = f
|
|
304
|
+
cls.__dataclass_fields__[name] = f
|
|
305
|
+
|
|
306
|
+
def create_field(self, name: str, value: Any) -> None:
|
|
307
|
+
"""create_field.
|
|
308
|
+
create a new Field on Model (when strict is False).
|
|
309
|
+
Args:
|
|
310
|
+
name (str): name of the field
|
|
311
|
+
value (Any): value to be assigned.
|
|
312
|
+
Raises:
|
|
313
|
+
TypeError: when try to create a new field on an Strict Model.
|
|
314
|
+
"""
|
|
315
|
+
if self.Meta.strict is True:
|
|
316
|
+
raise TypeError(
|
|
317
|
+
f'Cannot create a new field {name} on a Strict Model.'
|
|
318
|
+
)
|
|
319
|
+
if name != '__errors__':
|
|
320
|
+
f = Field(required=False, default=value)
|
|
321
|
+
f.name = name
|
|
322
|
+
f.type = type(value)
|
|
323
|
+
f._field_type = _FIELD
|
|
324
|
+
self.__columns__[name] = f
|
|
325
|
+
self.__dataclass_fields__[name] = f
|
|
326
|
+
setattr(self, name, value)
|
|
327
|
+
|
|
328
|
+
def set(self, name: str, value: Any) -> None:
|
|
329
|
+
"""set.
|
|
330
|
+
Alias for Create Field.
|
|
331
|
+
Args:
|
|
332
|
+
name (str): name of the field
|
|
333
|
+
value (Any): value to be assigned.
|
|
334
|
+
"""
|
|
335
|
+
if name not in self.__columns__:
|
|
336
|
+
if name != '__errors__' and self.Meta.strict is False:
|
|
337
|
+
self.create_field(name, value)
|
|
338
|
+
else:
|
|
339
|
+
setattr(self, name, value)
|
|
340
|
+
|
|
341
|
+
def get_errors(self):
|
|
342
|
+
return self.__errors__
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def make_model(cls, name: str, schema: str = "public", fields: list = None):
|
|
346
|
+
parent = inspect.getmro(cls)
|
|
347
|
+
obj = make_dataclass(name, fields, bases=(parent[0],))
|
|
348
|
+
m = Meta()
|
|
349
|
+
m.name = name
|
|
350
|
+
m.schema = schema
|
|
351
|
+
m.app_label = schema
|
|
352
|
+
obj.Meta = m
|
|
353
|
+
return obj
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def from_json(cls, obj: str, **kwargs) -> dataclass:
|
|
357
|
+
try:
|
|
358
|
+
decoder = cls.__encoder__(**kwargs)
|
|
359
|
+
decoded = decoder.loads(obj)
|
|
360
|
+
return cls(**decoded)
|
|
361
|
+
except ValueError as e:
|
|
362
|
+
raise RuntimeError(
|
|
363
|
+
"DataModel: Invalid string (JSON) data for decoding: {e}"
|
|
364
|
+
) from e
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def from_dict(cls, obj: dict) -> dataclass:
|
|
368
|
+
try:
|
|
369
|
+
return cls(**obj)
|
|
370
|
+
except ValueError as e:
|
|
371
|
+
raise RuntimeError(
|
|
372
|
+
"DataModel: Invalid Dictionary data for decoding: {e}"
|
|
373
|
+
) from e
|
|
374
|
+
|
|
375
|
+
@classmethod
|
|
376
|
+
def model(cls, dialect: str = "json", **kwargs) -> Any:
|
|
377
|
+
"""model.
|
|
378
|
+
|
|
379
|
+
Return the json-version of current Model.
|
|
380
|
+
Returns:
|
|
381
|
+
str: string (json) version of model.
|
|
382
|
+
"""
|
|
383
|
+
result = None
|
|
384
|
+
clsname = cls.__name__
|
|
385
|
+
schema = cls.Meta.schema
|
|
386
|
+
table = cls.Meta.name if cls.Meta.name else clsname.lower()
|
|
387
|
+
columns = cls.columns(cls).items()
|
|
388
|
+
if dialect == 'json':
|
|
389
|
+
cols = {}
|
|
390
|
+
for _, field in columns:
|
|
391
|
+
key = field.name
|
|
392
|
+
_type = field.type
|
|
393
|
+
if _type.__module__ == 'typing':
|
|
394
|
+
# TODO: discover real value of typing
|
|
395
|
+
if _type._name == 'List':
|
|
396
|
+
t = 'array'
|
|
397
|
+
elif _type._name == 'Dict':
|
|
398
|
+
t = 'object'
|
|
399
|
+
else:
|
|
400
|
+
try:
|
|
401
|
+
t = _type.__args__[0]
|
|
402
|
+
t = t.__name__
|
|
403
|
+
except (AttributeError, ValueError):
|
|
404
|
+
t = 'object'
|
|
405
|
+
else:
|
|
406
|
+
try:
|
|
407
|
+
t = JSON_TYPES[_type]
|
|
408
|
+
except KeyError:
|
|
409
|
+
t = 'object'
|
|
410
|
+
cols[key] = {"name": key, "type": t}
|
|
411
|
+
doc = {
|
|
412
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
413
|
+
"$id": f"/schemas/{table}",
|
|
414
|
+
"name": clsname,
|
|
415
|
+
"description": cls.__doc__.strip("\n").strip(),
|
|
416
|
+
"additionalProperties": False,
|
|
417
|
+
"table": table,
|
|
418
|
+
"schema": schema,
|
|
419
|
+
"type": "object",
|
|
420
|
+
"properties": cols,
|
|
421
|
+
}
|
|
422
|
+
encoder = cls.__encoder__(**kwargs)
|
|
423
|
+
result = encoder.dumps(doc, option=OPT_INDENT_2)
|
|
424
|
+
return result
|
|
425
|
+
|
|
426
|
+
@classmethod
|
|
427
|
+
def sample(cls) -> dict:
|
|
428
|
+
"""sample.
|
|
429
|
+
|
|
430
|
+
Get a dict (JSON) sample of this datamodel, based on default values.
|
|
431
|
+
|
|
432
|
+
Returns:
|
|
433
|
+
dict: _description_
|
|
434
|
+
"""
|
|
435
|
+
columns = cls.get_columns().items()
|
|
436
|
+
_fields = {}
|
|
437
|
+
required = []
|
|
438
|
+
for name, f in columns:
|
|
439
|
+
if f.repr is False:
|
|
440
|
+
continue
|
|
441
|
+
_fields[name] = f.default
|
|
442
|
+
try:
|
|
443
|
+
if f.metadata["required"] is True:
|
|
444
|
+
required.append(name)
|
|
445
|
+
except KeyError:
|
|
446
|
+
pass
|
|
447
|
+
return {
|
|
448
|
+
"properties": _fields,
|
|
449
|
+
"required": required
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
def _get_meta_value(self, key: str, fallback: Any = None, locale: Any = None):
|
|
453
|
+
value = getattr(self.Meta, key, fallback)
|
|
454
|
+
if locale is not None:
|
|
455
|
+
value = locale(value)
|
|
456
|
+
return value
|
|
457
|
+
|
|
458
|
+
def _get_metadata(self, field, key: str, locale: Any = None):
|
|
459
|
+
value = field.metadata.get(key, None)
|
|
460
|
+
if locale is not None:
|
|
461
|
+
value = locale(value)
|
|
462
|
+
return value
|
|
463
|
+
|
|
464
|
+
def _get_field_schema(
|
|
465
|
+
self,
|
|
466
|
+
type_info: str,
|
|
467
|
+
field: object,
|
|
468
|
+
description: str,
|
|
469
|
+
locale: Any = None,
|
|
470
|
+
**kwargs
|
|
471
|
+
) -> dict:
|
|
472
|
+
return {
|
|
473
|
+
"type": type_info,
|
|
474
|
+
"nullable": field.metadata.get('nullable', False),
|
|
475
|
+
"attrs": {
|
|
476
|
+
"placeholder": description,
|
|
477
|
+
"format": field.metadata.get('format', None),
|
|
478
|
+
},
|
|
479
|
+
"readOnly": field.metadata.get('readonly', False),
|
|
480
|
+
**kwargs
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
@classmethod
|
|
484
|
+
def schema(cls, as_dict=False, locale: Any = None):
|
|
485
|
+
"""Convert Model to JSON-Schema.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
as_dict (bool, optional): if false, Returns JSON-schema as a JSON object.
|
|
489
|
+
Defaults to False.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
_type_: JSON-Schema version of Model.
|
|
493
|
+
"""
|
|
494
|
+
# description:
|
|
495
|
+
description = cls._get_meta_value(
|
|
496
|
+
cls,
|
|
497
|
+
'description',
|
|
498
|
+
fallback=cls.__doc__.strip("\n").strip(),
|
|
499
|
+
locale=locale
|
|
500
|
+
)
|
|
501
|
+
title = cls._get_meta_value(
|
|
502
|
+
cls,
|
|
503
|
+
'title',
|
|
504
|
+
fallback=cls.__name__,
|
|
505
|
+
locale=locale
|
|
506
|
+
)
|
|
507
|
+
try:
|
|
508
|
+
title = slugify_camelcase(title)
|
|
509
|
+
except Exception:
|
|
510
|
+
pass
|
|
511
|
+
|
|
512
|
+
# Table Name:
|
|
513
|
+
table = cls.Meta.name.lower() if cls.Meta.name else title.lower()
|
|
514
|
+
endpoint = cls.Meta.endpoint
|
|
515
|
+
schema = cls.Meta.schema
|
|
516
|
+
columns = cls.get_columns().items()
|
|
517
|
+
|
|
518
|
+
fields = {}
|
|
519
|
+
required = []
|
|
520
|
+
defs = {}
|
|
521
|
+
|
|
522
|
+
# settings:
|
|
523
|
+
settings = cls._get_meta_value(
|
|
524
|
+
cls,
|
|
525
|
+
'settings',
|
|
526
|
+
fallback={},
|
|
527
|
+
locale=None
|
|
528
|
+
)
|
|
529
|
+
try:
|
|
530
|
+
settings = {
|
|
531
|
+
"settings": settings
|
|
532
|
+
}
|
|
533
|
+
except TypeError:
|
|
534
|
+
settings = {}
|
|
535
|
+
|
|
536
|
+
for name, field in columns:
|
|
537
|
+
_type = field.type
|
|
538
|
+
type_info = _get_type_info(_type, name, title)
|
|
539
|
+
ref_info = _get_ref_info(_type, field)
|
|
540
|
+
if ref_info:
|
|
541
|
+
defs[name] = ref_info.pop('schema', None)
|
|
542
|
+
else:
|
|
543
|
+
ref_info = {}
|
|
544
|
+
|
|
545
|
+
minimum = field.metadata.get('min', None)
|
|
546
|
+
maximum = field.metadata.get('max', None)
|
|
547
|
+
secret = field.metadata.get('secret', None)
|
|
548
|
+
# custom endpoint for every field:
|
|
549
|
+
custom_endpoint = field.metadata.get('endpoint', None)
|
|
550
|
+
|
|
551
|
+
if field.metadata.get('required', False) or field.metadata.get('primary', False):
|
|
552
|
+
required.append(name)
|
|
553
|
+
|
|
554
|
+
# UI objects:
|
|
555
|
+
ui_objects = {
|
|
556
|
+
k.replace('_', ':'): v for k, v in field.metadata.items() if k.startswith('ui_')
|
|
557
|
+
}
|
|
558
|
+
# schema_extra:
|
|
559
|
+
schema_extra = field.metadata.get('schema_extra', {})
|
|
560
|
+
meta_description = cls._get_metadata(
|
|
561
|
+
cls,
|
|
562
|
+
field,
|
|
563
|
+
key='description',
|
|
564
|
+
locale=locale
|
|
565
|
+
)
|
|
566
|
+
fields[name] = cls._get_field_schema(
|
|
567
|
+
cls,
|
|
568
|
+
type_info,
|
|
569
|
+
field,
|
|
570
|
+
description=meta_description,
|
|
571
|
+
locale=locale,
|
|
572
|
+
**ui_objects,
|
|
573
|
+
**schema_extra,
|
|
574
|
+
**ref_info
|
|
575
|
+
)
|
|
576
|
+
label = cls._get_metadata(cls, field, 'label', locale=locale)
|
|
577
|
+
if label:
|
|
578
|
+
fields[name]["label"] = label
|
|
579
|
+
if meta_description:
|
|
580
|
+
fields[name]["description"] = meta_description
|
|
581
|
+
if custom_endpoint:
|
|
582
|
+
fields[name]["endpoint"] = custom_endpoint
|
|
583
|
+
|
|
584
|
+
if 'write_only' in field.metadata:
|
|
585
|
+
fields[name]["writeOnly"] = field.metadata.get('write_only', False)
|
|
586
|
+
|
|
587
|
+
if 'pattern' in field.metadata:
|
|
588
|
+
fields[name]["attrs"]["pattern"] = field.metadata['pattern']
|
|
589
|
+
|
|
590
|
+
if field.repr is False:
|
|
591
|
+
fields[name]["attrs"]["visible"] = False
|
|
592
|
+
|
|
593
|
+
if field.default:
|
|
594
|
+
d = field.default
|
|
595
|
+
if is_callable(d):
|
|
596
|
+
fields[name]['default'] = f"fn:{d!r}"
|
|
597
|
+
else:
|
|
598
|
+
fields[name]['default'] = f"{d!s}"
|
|
599
|
+
|
|
600
|
+
if secret is not None:
|
|
601
|
+
fields[name]['secret'] = secret
|
|
602
|
+
|
|
603
|
+
if type_info == 'string':
|
|
604
|
+
if minimum:
|
|
605
|
+
fields[name]['minLength'] = minimum
|
|
606
|
+
if maximum:
|
|
607
|
+
fields[name]['maxLength'] = maximum
|
|
608
|
+
else:
|
|
609
|
+
if minimum:
|
|
610
|
+
fields[name]['minimum'] = minimum
|
|
611
|
+
if maximum:
|
|
612
|
+
fields[name]['maximum'] = maximum
|
|
613
|
+
|
|
614
|
+
endpoint_kwargs = {}
|
|
615
|
+
if endpoint:
|
|
616
|
+
endpoint_kwargs["endpoint"] = endpoint
|
|
617
|
+
|
|
618
|
+
base_schema = {
|
|
619
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
620
|
+
"$id": f"/schemas/{table}",
|
|
621
|
+
**endpoint_kwargs,
|
|
622
|
+
**settings,
|
|
623
|
+
"additionalProperties": cls.Meta.strict,
|
|
624
|
+
"title": title,
|
|
625
|
+
"description": description,
|
|
626
|
+
"type": "object",
|
|
627
|
+
"table": table,
|
|
628
|
+
"schema": schema,
|
|
629
|
+
"properties": fields,
|
|
630
|
+
"required": required,
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if defs:
|
|
634
|
+
base_schema["$defs"] = defs
|
|
635
|
+
|
|
636
|
+
if as_dict is True:
|
|
637
|
+
return base_schema
|
|
638
|
+
else:
|
|
639
|
+
return json_encoder(base_schema)
|