python-datamodel 0.8.13__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 +337 -0
- datamodel/aliases/__init__.py +26 -0
- datamodel/base.py +182 -0
- datamodel/converters.c +40958 -0
- datamodel/converters.cp313-win_amd64.pyd +0 -0
- datamodel/exceptions.c +13455 -0
- datamodel/exceptions.cp313-win_amd64.pyd +0 -0
- datamodel/fields.cp313-win_amd64.pyd +0 -0
- datamodel/fields.cpp +17289 -0
- datamodel/functions.cp313-win_amd64.pyd +0 -0
- datamodel/functions.cpp +8940 -0
- datamodel/jsonld/__init__.py +45 -0
- datamodel/jsonld/models.py +500 -0
- datamodel/libs/__init__.py +0 -0
- datamodel/libs/mapping.c +15142 -0
- datamodel/libs/mapping.cp313-win_amd64.pyd +0 -0
- datamodel/libs/mutables.py +128 -0
- datamodel/models.py +787 -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 +13008 -0
- datamodel/profiler.py +21 -0
- datamodel/py.typed +0 -0
- datamodel/types.c +7165 -0
- datamodel/types.cp313-win_amd64.pyd +0 -0
- datamodel/validation.cp313-win_amd64.pyd +0 -0
- datamodel/validation.cpp +14238 -0
- datamodel/version.py +13 -0
- python_datamodel-0.8.13.dist-info/LICENSE +29 -0
- python_datamodel-0.8.13.dist-info/METADATA +316 -0
- python_datamodel-0.8.13.dist-info/RECORD +35 -0
- python_datamodel-0.8.13.dist-info/WHEEL +5 -0
- python_datamodel-0.8.13.dist-info/top_level.txt +1 -0
datamodel/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""DataModels.
|
|
3
|
+
|
|
4
|
+
DataModel is a reimplementation of dataclasses with true inheritance and composition.
|
|
5
|
+
"""
|
|
6
|
+
from datamodel.fields import Field, Column, fields
|
|
7
|
+
from .models import Model
|
|
8
|
+
from .base import BaseModel
|
|
9
|
+
from .version import (
|
|
10
|
+
__title__, __description__, __version__, __author__, __author_email__
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = ('fields', 'Field', 'Column', 'Model', 'BaseModel', )
|
datamodel/abstract.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional, Any, List, Dict, get_args, get_origin
|
|
3
|
+
from types import GenericAlias
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
import types
|
|
7
|
+
from inspect import isclass
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from .parsers.json import JSONContent
|
|
10
|
+
from .converters import encoders, parse_basic, parse_type
|
|
11
|
+
from .fields import Field
|
|
12
|
+
from .functions import (
|
|
13
|
+
is_dataclass,
|
|
14
|
+
is_primitive
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
class Meta:
|
|
18
|
+
"""
|
|
19
|
+
Metadata information about Model.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
name: str = "" name of the model
|
|
23
|
+
description: str = "" description of the model
|
|
24
|
+
schema: str = "" schema of the model (optional)
|
|
25
|
+
frozen: bool = False if the model (dataclass) is read-only (frozen state)
|
|
26
|
+
strict: bool = True if the model (dataclass) should raise an error on invalid data.
|
|
27
|
+
remove_null: bool = True if the model should remove null values from the data.
|
|
28
|
+
validate_assignment: bool = True if the model should validate during assignment.
|
|
29
|
+
"""
|
|
30
|
+
name: str = ""
|
|
31
|
+
description: str = ""
|
|
32
|
+
schema: str = ""
|
|
33
|
+
frozen: bool = False
|
|
34
|
+
strict: bool = True
|
|
35
|
+
driver: str = None
|
|
36
|
+
credentials: dict = Optional[dict]
|
|
37
|
+
dsn: Optional[str] = None
|
|
38
|
+
datasource: Optional[str] = None
|
|
39
|
+
connection: Optional[Callable] = None
|
|
40
|
+
remove_nulls: bool = False
|
|
41
|
+
endpoint: str
|
|
42
|
+
extra: str = 'forbid' # could be 'allow', 'ignore', or 'forbid'
|
|
43
|
+
validate_assignment: bool = False
|
|
44
|
+
as_objects: bool = False
|
|
45
|
+
no_nesting: bool = False
|
|
46
|
+
alias_function: Optional[Callable] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_connection(cls, conn: Callable):
|
|
50
|
+
cls.connection = conn
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _dc_method_setattr_(
|
|
54
|
+
self,
|
|
55
|
+
name: str,
|
|
56
|
+
value: Any,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
_dc_method_setattr_.
|
|
60
|
+
- Method for overwrite the "setattr" on Dataclasses.
|
|
61
|
+
"""
|
|
62
|
+
# Initialize __values__ if it doesn't exist
|
|
63
|
+
if not hasattr(self, '__values__'):
|
|
64
|
+
object.__setattr__(self, '__values__', {})
|
|
65
|
+
|
|
66
|
+
# If the attribute name is already a known field, proceed normally
|
|
67
|
+
if name in self.__fields__:
|
|
68
|
+
# Only store the initial value:
|
|
69
|
+
if name not in self.__values__:
|
|
70
|
+
# Store the initial value in __values__
|
|
71
|
+
self.__values__[name] = value
|
|
72
|
+
if self.Meta.validate_assignment:
|
|
73
|
+
try:
|
|
74
|
+
# re-apply the parse/validation of this field:
|
|
75
|
+
field_category = self.__field_types__.get(name, 'complex')
|
|
76
|
+
field_obj = self.__columns__[name]
|
|
77
|
+
_type = field_obj.type
|
|
78
|
+
_encoder = field_obj.metadata.get('encoder')
|
|
79
|
+
if field_category == 'primitive':
|
|
80
|
+
new_val = parse_basic(_type, value, _encoder)
|
|
81
|
+
elif field_category == 'typing':
|
|
82
|
+
new_val = parse_type(field_obj, _type, value, _encoder)
|
|
83
|
+
elif field_category in ('dataclass', 'class', ):
|
|
84
|
+
new_val = value
|
|
85
|
+
else:
|
|
86
|
+
new_val = parse_type(field_obj, _type, value, _encoder)
|
|
87
|
+
# Assign the new value to the field
|
|
88
|
+
value = new_val
|
|
89
|
+
except Exception as e:
|
|
90
|
+
# Raise or re-raise a TypeError or something meaningful
|
|
91
|
+
raise TypeError(
|
|
92
|
+
f"Cannot assign {value!r} to field {name!r}: {e}"
|
|
93
|
+
) from e
|
|
94
|
+
object.__setattr__(self, name, value)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
if self.Meta.frozen is True and name not in self.__fields__:
|
|
98
|
+
raise TypeError(
|
|
99
|
+
f"Cannot add New attribute {name} on {self.modelName}, "
|
|
100
|
+
"This DataClass is frozen (read-only class)"
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
# try to dynamically add a field or store the attribute
|
|
104
|
+
value = None if callable(value) else value
|
|
105
|
+
object.__setattr__(self, name, value)
|
|
106
|
+
if name == '__values__':
|
|
107
|
+
return
|
|
108
|
+
if name not in self.__fields__:
|
|
109
|
+
if self.Meta.strict is True:
|
|
110
|
+
return False
|
|
111
|
+
# If it’s not a known field, consult self.Meta.extra
|
|
112
|
+
extra_policy = self.Meta.extra
|
|
113
|
+
if extra_policy == 'forbid':
|
|
114
|
+
raise TypeError(
|
|
115
|
+
f"Field {name!r} is not allowed on {self.modelName}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
if extra_policy == 'ignore':
|
|
119
|
+
# do nothing, skip silently
|
|
120
|
+
return
|
|
121
|
+
try:
|
|
122
|
+
# create a new Field on Model.
|
|
123
|
+
f = Field(required=False, default=value)
|
|
124
|
+
f.name = name
|
|
125
|
+
f.type = type(value)
|
|
126
|
+
self.__columns__[name] = f
|
|
127
|
+
self.__fields__.append(name)
|
|
128
|
+
setattr(self, name, value)
|
|
129
|
+
except Exception as err:
|
|
130
|
+
logging.exception(err, stack_info=True)
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ModelMeta(type):
|
|
135
|
+
"""ModelMeta.
|
|
136
|
+
|
|
137
|
+
Metaclass for DataModels, convert any Model into a dataclass-compatible object.
|
|
138
|
+
"""
|
|
139
|
+
__columns__: Dict
|
|
140
|
+
__fields__: List
|
|
141
|
+
__field_types__: List
|
|
142
|
+
__aliases__: Dict
|
|
143
|
+
|
|
144
|
+
def __new__(cls, name, bases, attrs, **kwargs): # noqa
|
|
145
|
+
cols = OrderedDict()
|
|
146
|
+
strict = False
|
|
147
|
+
cls.__field_types__ = {}
|
|
148
|
+
cls.__typing_args__ = {}
|
|
149
|
+
cls.__aliases__ = {}
|
|
150
|
+
_types = {}
|
|
151
|
+
_typing_args = {}
|
|
152
|
+
aliases = {}
|
|
153
|
+
|
|
154
|
+
if "__annotations__" in attrs:
|
|
155
|
+
annotations = attrs.get('__annotations__', {})
|
|
156
|
+
try:
|
|
157
|
+
strict = attrs['Meta'].strict
|
|
158
|
+
except (TypeError, AttributeError, KeyError):
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _initialize_fields(attrs, annotations, strict):
|
|
163
|
+
cols = OrderedDict()
|
|
164
|
+
_types_local = {}
|
|
165
|
+
_typing_args = {}
|
|
166
|
+
aliases = {}
|
|
167
|
+
for field, _type in annotations.items():
|
|
168
|
+
if isinstance(_type, Field):
|
|
169
|
+
_type = _type.type
|
|
170
|
+
df = attrs.get(
|
|
171
|
+
field,
|
|
172
|
+
Field(type=_type, required=False, default=None)
|
|
173
|
+
)
|
|
174
|
+
if df is not None and isinstance(df, Field):
|
|
175
|
+
alias = df.metadata.get("alias", None)
|
|
176
|
+
if alias:
|
|
177
|
+
aliases[alias] = field
|
|
178
|
+
if not isinstance(df, Field):
|
|
179
|
+
df = Field(required=False, type=_type, default=df)
|
|
180
|
+
df.name = field
|
|
181
|
+
df.type = _type
|
|
182
|
+
try:
|
|
183
|
+
df._encoder_fn = encoders[_type]
|
|
184
|
+
except (TypeError, KeyError):
|
|
185
|
+
df._encoder_fn = None
|
|
186
|
+
|
|
187
|
+
# Cache reflection info so we DON’T need to call
|
|
188
|
+
# get_origin/get_args repeatedly:
|
|
189
|
+
origin = get_origin(_type)
|
|
190
|
+
args = get_args(_type)
|
|
191
|
+
_default = df.default
|
|
192
|
+
_is_dc = is_dataclass(_type)
|
|
193
|
+
_is_prim = is_primitive(_type)
|
|
194
|
+
_is_alias = isinstance(_type, GenericAlias)
|
|
195
|
+
_is_typing = hasattr(_type, '__module__') and _type.__module__ == 'typing' # noqa
|
|
196
|
+
|
|
197
|
+
# Store the type info in the field object:
|
|
198
|
+
df.is_dc = _is_dc
|
|
199
|
+
df.is_primitive = _is_prim
|
|
200
|
+
df.is_typing = _is_typing
|
|
201
|
+
df.origin = origin
|
|
202
|
+
df.args = args
|
|
203
|
+
df.type_args = getattr(_type, '__args__', None)
|
|
204
|
+
|
|
205
|
+
df._typeinfo_ = {
|
|
206
|
+
"default_callable": callable(_default)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# check type of field:
|
|
210
|
+
if _is_prim:
|
|
211
|
+
_type_category = 'primitive'
|
|
212
|
+
elif origin == type:
|
|
213
|
+
_type_category = 'typing'
|
|
214
|
+
elif _is_dc:
|
|
215
|
+
_type_category = 'dataclass'
|
|
216
|
+
elif _is_typing: # noqa
|
|
217
|
+
_type_category = 'typing'
|
|
218
|
+
elif isclass(_type):
|
|
219
|
+
_type_category = 'class'
|
|
220
|
+
elif _is_alias:
|
|
221
|
+
_type_category = 'typing'
|
|
222
|
+
else:
|
|
223
|
+
_type_category = 'complex'
|
|
224
|
+
_types_local[field] = _type_category
|
|
225
|
+
df._type_category = _type_category
|
|
226
|
+
|
|
227
|
+
# Store them in a dict keyed by field name:
|
|
228
|
+
_typing_args[field] = (origin, args)
|
|
229
|
+
# Assign the field object to the attrs so dataclass can pick it up
|
|
230
|
+
attrs[field] = df
|
|
231
|
+
cols[field] = df
|
|
232
|
+
return cols, _types_local, _typing_args, aliases
|
|
233
|
+
|
|
234
|
+
# Initialize the fields
|
|
235
|
+
cols, _types, _typing_args, aliases = _initialize_fields(
|
|
236
|
+
attrs, annotations, strict
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
# if no __annotations__, cols is empty:
|
|
240
|
+
cols = OrderedDict()
|
|
241
|
+
|
|
242
|
+
_columns = cols.keys()
|
|
243
|
+
cls.__slots__ = tuple(_columns)
|
|
244
|
+
|
|
245
|
+
# Pop Meta before creating the class so we can assign it after
|
|
246
|
+
attr_meta = attrs.pop("Meta", None)
|
|
247
|
+
# Create the class
|
|
248
|
+
new_cls = super().__new__(cls, name, bases, attrs, **kwargs)
|
|
249
|
+
|
|
250
|
+
# Attach Meta class
|
|
251
|
+
new_cls.Meta = attr_meta or getattr(new_cls, "Meta", Meta)
|
|
252
|
+
new_cls.__dataclass_fields__ = cols
|
|
253
|
+
new_cls.__typing_args__ = _typing_args
|
|
254
|
+
if not new_cls.Meta:
|
|
255
|
+
new_cls.Meta = Meta
|
|
256
|
+
new_cls.Meta.set_connection = types.MethodType(
|
|
257
|
+
set_connection, new_cls.Meta
|
|
258
|
+
)
|
|
259
|
+
try:
|
|
260
|
+
frozen = new_cls.Meta.frozen
|
|
261
|
+
except AttributeError:
|
|
262
|
+
new_cls.Meta.frozen = False
|
|
263
|
+
frozen = False
|
|
264
|
+
|
|
265
|
+
# mix values from Meta to an existing Meta Class
|
|
266
|
+
new_cls.Meta.__annotations__ = Meta.__annotations__
|
|
267
|
+
for key, _ in Meta.__annotations__.items():
|
|
268
|
+
if not hasattr(new_cls.Meta, key):
|
|
269
|
+
try:
|
|
270
|
+
setattr(new_cls.Meta, key, None)
|
|
271
|
+
except AttributeError as e:
|
|
272
|
+
logging.warning(
|
|
273
|
+
f'Missing Meta Key: {key}, {e}'
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# If there's a __model_init__ method, call it
|
|
277
|
+
try:
|
|
278
|
+
new_cls.__model_init__(
|
|
279
|
+
new_cls,
|
|
280
|
+
name,
|
|
281
|
+
attrs
|
|
282
|
+
)
|
|
283
|
+
except AttributeError:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
# Now that fields are in attrs, decorate the class as a dataclass
|
|
287
|
+
dc = dataclass(
|
|
288
|
+
unsafe_hash=strict,
|
|
289
|
+
repr=False,
|
|
290
|
+
init=True,
|
|
291
|
+
order=False,
|
|
292
|
+
eq=True,
|
|
293
|
+
frozen=frozen
|
|
294
|
+
)(new_cls)
|
|
295
|
+
# Set additional attributes:
|
|
296
|
+
dc.__columns__ = cols
|
|
297
|
+
dc.__fields__ = list(_columns)
|
|
298
|
+
dc.__values__ = {}
|
|
299
|
+
dc.__encoder__ = JSONContent
|
|
300
|
+
dc.__valid__ = False
|
|
301
|
+
dc.__errors__ = None
|
|
302
|
+
dc.__frozen__ = strict
|
|
303
|
+
dc.__initialised__ = False
|
|
304
|
+
dc.__field_types__ = _types
|
|
305
|
+
dc.__aliases__ = aliases
|
|
306
|
+
dc.__typing_args__ = _typing_args
|
|
307
|
+
dc.modelName = dc.__name__
|
|
308
|
+
|
|
309
|
+
# Override __setattr__ method
|
|
310
|
+
setattr(dc, "__setattr__", _dc_method_setattr_)
|
|
311
|
+
return dc
|
|
312
|
+
|
|
313
|
+
def __init__(cls, *args, **kwargs) -> None:
|
|
314
|
+
# Initialized Data Model = True
|
|
315
|
+
cls.__initialised__ = True
|
|
316
|
+
cls.__errors__ = None
|
|
317
|
+
super().__init__(*args, **kwargs)
|
|
318
|
+
|
|
319
|
+
def __call__(cls, *args, **kwargs):
|
|
320
|
+
# rename any kwargs that match an alias ONLY if there are aliases defined.
|
|
321
|
+
alias_func = getattr(cls.Meta, "alias_function", None)
|
|
322
|
+
if callable(alias_func):
|
|
323
|
+
new_kwargs = {}
|
|
324
|
+
for k, v in kwargs.items():
|
|
325
|
+
new_k = alias_func(k)
|
|
326
|
+
new_kwargs[new_k] = v
|
|
327
|
+
kwargs = new_kwargs
|
|
328
|
+
if cls.__aliases__:
|
|
329
|
+
new_kwargs = {}
|
|
330
|
+
for k, v in kwargs.items():
|
|
331
|
+
if k in cls.__aliases__:
|
|
332
|
+
real_field = cls.__aliases__[k]
|
|
333
|
+
new_kwargs[real_field] = v
|
|
334
|
+
else:
|
|
335
|
+
new_kwargs[k] = v
|
|
336
|
+
kwargs = new_kwargs
|
|
337
|
+
return super().__call__(*args, **kwargs)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Aliases Functions:
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
_RE_FIRST_CAP = re.compile(r"(.)([A-Z][a-z]+)")
|
|
5
|
+
_RE_ALL_CAPS = re.compile(r"([a-z0-9])([A-Z])")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_snakecase(name: str) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Convert a CamelCase or PascalCase string into snake_case.
|
|
11
|
+
Example: "EmailAddress" -> "email_address"
|
|
12
|
+
"""
|
|
13
|
+
# Insert underscores before capital letters, then lower-case
|
|
14
|
+
s1 = _RE_FIRST_CAP.sub(r"\1_\2", name)
|
|
15
|
+
return _RE_ALL_CAPS.sub(r"\1_\2", s1).lower()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_pascalcase(s: str) -> str:
|
|
19
|
+
"""
|
|
20
|
+
Convert a snake_case string into PascalCase.
|
|
21
|
+
Example:
|
|
22
|
+
store_id -> StoreId
|
|
23
|
+
email_address -> EmailAddress
|
|
24
|
+
"""
|
|
25
|
+
parts = s.split("_")
|
|
26
|
+
return "".join(word.capitalize() for word in parts)
|
datamodel/base.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
# Dataclass
|
|
4
|
+
from dataclasses import (
|
|
5
|
+
_FIELD,
|
|
6
|
+
)
|
|
7
|
+
from html import escape
|
|
8
|
+
from .converters import process_attributes, register_converter
|
|
9
|
+
from .fields import Field
|
|
10
|
+
from .exceptions import ValidationError
|
|
11
|
+
from .abstract import ModelMeta
|
|
12
|
+
from .models import ModelMixin
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
TYPE_CONVERTERS = {}
|
|
16
|
+
|
|
17
|
+
RendererFn = Callable[["BaseModel", bool], str]
|
|
18
|
+
|
|
19
|
+
HTML_RENDERERS: Dict[str, RendererFn] = {}
|
|
20
|
+
|
|
21
|
+
def register_renderer(schema_type: str):
|
|
22
|
+
"""
|
|
23
|
+
Decorator to register a custom renderer function for a given schema_type.
|
|
24
|
+
"""
|
|
25
|
+
def decorator(fn: RendererFn):
|
|
26
|
+
HTML_RENDERERS[schema_type] = fn
|
|
27
|
+
return fn
|
|
28
|
+
return decorator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BaseModel(ModelMixin, metaclass=ModelMeta):
|
|
32
|
+
"""
|
|
33
|
+
BaseModel.
|
|
34
|
+
Base Model for all DataModels.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __post_init__(self) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Post init method.
|
|
40
|
+
Fill fields with function-factory or calling validations
|
|
41
|
+
"""
|
|
42
|
+
# checking if an attribute is already a dataclass:
|
|
43
|
+
columns = list(self.__columns__.items())
|
|
44
|
+
|
|
45
|
+
if errors := process_attributes(self, columns):
|
|
46
|
+
if self.Meta.strict is True:
|
|
47
|
+
raise ValidationError(
|
|
48
|
+
f"""{self.modelName}: There are errors in Model. \
|
|
49
|
+
Hint: please check the "payload" attribute in the exception.""",
|
|
50
|
+
payload=errors
|
|
51
|
+
)
|
|
52
|
+
self.__errors__ = errors
|
|
53
|
+
object.__setattr__(self, "__valid__", False)
|
|
54
|
+
else:
|
|
55
|
+
object.__setattr__(self, "__valid__", True)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def register_converter(
|
|
59
|
+
cls,
|
|
60
|
+
target_type: Any,
|
|
61
|
+
func: Callable,
|
|
62
|
+
field_name: str = None
|
|
63
|
+
):
|
|
64
|
+
key = (target_type, field_name) if field_name else target_type
|
|
65
|
+
register_converter(key, func)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def add_field(cls, name: str, value: Any = None) -> None:
|
|
69
|
+
if cls.Meta.strict is True:
|
|
70
|
+
raise TypeError(
|
|
71
|
+
f'Cannot create a new field {name} on a Strict Model.'
|
|
72
|
+
)
|
|
73
|
+
if name != '__errors__':
|
|
74
|
+
f = Field(required=False, default=value)
|
|
75
|
+
f.name = name
|
|
76
|
+
f.type = type(value)
|
|
77
|
+
f._field_type = _FIELD
|
|
78
|
+
cls.__columns__[name] = f
|
|
79
|
+
cls.__dataclass_fields__[name] = f
|
|
80
|
+
|
|
81
|
+
def create_field(self, name: str, value: Any) -> None:
|
|
82
|
+
"""create_field.
|
|
83
|
+
create a new Field on Model (when strict is False).
|
|
84
|
+
Args:
|
|
85
|
+
name (str): name of the field
|
|
86
|
+
value (Any): value to be assigned.
|
|
87
|
+
Raises:
|
|
88
|
+
TypeError: when try to create a new field on an Strict Model.
|
|
89
|
+
"""
|
|
90
|
+
if self.Meta.strict is True:
|
|
91
|
+
raise TypeError(
|
|
92
|
+
f'Cannot create a new field {name} on a Strict Model.'
|
|
93
|
+
)
|
|
94
|
+
if name != '__errors__':
|
|
95
|
+
f = Field(required=False, default=value)
|
|
96
|
+
f.name = name
|
|
97
|
+
f.type = type(value)
|
|
98
|
+
f._field_type = _FIELD
|
|
99
|
+
self.__columns__[name] = f
|
|
100
|
+
self.__dataclass_fields__[name] = f
|
|
101
|
+
setattr(self, name, value)
|
|
102
|
+
|
|
103
|
+
def set(self, name: str, value: Any) -> None:
|
|
104
|
+
"""set.
|
|
105
|
+
Alias for Create Field.
|
|
106
|
+
Args:
|
|
107
|
+
name (str): name of the field
|
|
108
|
+
value (Any): value to be assigned.
|
|
109
|
+
"""
|
|
110
|
+
if name in self.__columns__:
|
|
111
|
+
setattr(self, name, value)
|
|
112
|
+
elif name != '__errors__' and self.Meta.strict is False:
|
|
113
|
+
self.create_field(name, value)
|
|
114
|
+
|
|
115
|
+
def get_errors(self):
|
|
116
|
+
return self.__errors__
|
|
117
|
+
|
|
118
|
+
def to_html(self, top_level: bool = True) -> str:
|
|
119
|
+
"""to_html.
|
|
120
|
+
Convert Model to HTML.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
top_level (bool, optional): If True, adds the @context to the schema.
|
|
124
|
+
"""
|
|
125
|
+
# 1) Determine the schema type from self.Meta or fallback to class name
|
|
126
|
+
schema_type = getattr(self.Meta, 'schema_type', self.__class__.__name__)
|
|
127
|
+
|
|
128
|
+
if schema_type in HTML_RENDERERS:
|
|
129
|
+
return HTML_RENDERERS[schema_type](self, top_level)
|
|
130
|
+
|
|
131
|
+
# 2) Container opening. For top-level objects, we specify:
|
|
132
|
+
# - vocab="https://schema.org/"
|
|
133
|
+
# - typeof="Recipe" (or other type)
|
|
134
|
+
# For nested objects, we might omit the 'vocab' attribute
|
|
135
|
+
# or rely on the parent's scope.
|
|
136
|
+
if top_level:
|
|
137
|
+
container_open = f'<div vocab="https://schema.org/" typeof="{escape(schema_type)}">'
|
|
138
|
+
else:
|
|
139
|
+
container_open = f'<div property="{escape(schema_type)}" typeof="{escape(schema_type)}">'
|
|
140
|
+
|
|
141
|
+
# We'll accumulate our HTML pieces here
|
|
142
|
+
pieces = [container_open]
|
|
143
|
+
|
|
144
|
+
# 3) Iterate over each field in this model
|
|
145
|
+
for field_name, value in self.__dict__.items():
|
|
146
|
+
# Skip internal or error fields
|
|
147
|
+
if field_name.startswith('_') or field_name == '__errors__':
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Optionally skip if None
|
|
151
|
+
if value is None:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# 4) If value is a nested model, convert it to HTML as well
|
|
155
|
+
if isinstance(value, BaseModel):
|
|
156
|
+
nested_html = value.to_html(False)
|
|
157
|
+
snippet = f'<div property="{escape(field_name)}">\n{nested_html}\n</div>'
|
|
158
|
+
pieces.append(snippet)
|
|
159
|
+
|
|
160
|
+
elif isinstance(value, list):
|
|
161
|
+
# We might iterate and produce multiple lines
|
|
162
|
+
for item in value:
|
|
163
|
+
if isinstance(item, BaseModel):
|
|
164
|
+
nested_html = item.to_html(False)
|
|
165
|
+
snippet = f'<div property="{escape(field_name)}">\n{nested_html}\n</div>'
|
|
166
|
+
else:
|
|
167
|
+
# If it's a simple scalar, just output a <span>
|
|
168
|
+
# e.g.: <span property="recipeIngredient">3 bananas</span>
|
|
169
|
+
val_escaped = escape(str(item))
|
|
170
|
+
snippet = f'<span property="{escape(field_name)}">{val_escaped}</span>'
|
|
171
|
+
pieces.append(snippet)
|
|
172
|
+
else:
|
|
173
|
+
# For simple scalars (str, int, etc.):
|
|
174
|
+
# We might choose <span> or <meta> based on type.
|
|
175
|
+
# We'll do something simple: a <span>
|
|
176
|
+
val_escaped = escape(str(value))
|
|
177
|
+
snippet = f'<span property="{escape(field_name)}">{val_escaped}</span>'
|
|
178
|
+
pieces.append(snippet)
|
|
179
|
+
|
|
180
|
+
# 4) Close the container
|
|
181
|
+
pieces.append('</div>')
|
|
182
|
+
return "\n".join(pieces)
|