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/models.py ADDED
@@ -0,0 +1,787 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Dict
3
+ from enum import Enum, EnumMeta
4
+ # Dataclass
5
+ import inspect
6
+ from dataclasses import asdict as as_dict, dataclass, make_dataclass, _MISSING_TYPE
7
+ from operator import attrgetter
8
+ from orjson import OPT_INDENT_2
9
+ from datamodel.fields import fields
10
+ from .abstract import ModelMeta, Meta
11
+ from .fields import Field
12
+ from .parsers.encoders import json_encoder
13
+ from .converters import slugify_camelcase
14
+ from .types import JSON_TYPES, Text
15
+ from .functions import is_callable
16
+
17
+
18
+ def _get_type_info(_type, name, title):
19
+ if _type.__module__ == 'typing':
20
+ if inspect.isfunction(_type):
21
+ if hasattr(_type, '__supertype__'):
22
+ return _type.__supertype__
23
+ raise ValueError(
24
+ f"You're using bare Functions to type hint on {name} for: {title}"
25
+ )
26
+ if _type._name == 'List':
27
+ return 'array'
28
+ if _type._name == 'Dict':
29
+ return 'object'
30
+ try:
31
+ return _type.__args__[0].__name__
32
+ except (AttributeError, ValueError):
33
+ return 'string'
34
+ elif hasattr(_type, '__supertype__'):
35
+ if type(_type) == type(Text): # pylint: disable=C0123 # noqa
36
+ return 'text'
37
+ if isinstance(_type.__supertype__, (str, int)):
38
+ return 'string' if isinstance(_type.__supertype__, str) else 'integer'
39
+ return JSON_TYPES.get(_type, 'string')
40
+
41
+
42
+ def _get_ref_info(_type, field):
43
+ if isinstance(_type, EnumMeta):
44
+ return {
45
+ "type": "array",
46
+ "enum_type": {
47
+ "type": "string",
48
+ "enum": list(map(lambda c: c.value, _type))
49
+ }
50
+ }
51
+ elif isinstance(_type, ModelMeta):
52
+ _schema = _type.schema(as_dict=True)
53
+ columns = []
54
+ if 'fk' not in field.metadata:
55
+ ref = _schema.get('$id', f"/{_type.__name__}")
56
+ else:
57
+ columns = field.metadata.get('fk').split("|")
58
+ _id, _value = columns
59
+ ref = {
60
+ "api": field.metadata.get('api', _schema['table']),
61
+ "id": _id,
62
+ "value": _value,
63
+ "$ref": _schema.get('$id', f"/{_type.__name__}")
64
+ }
65
+ return {
66
+ "type": "object",
67
+ "schema": _schema,
68
+ "$ref": ref,
69
+ "columns": columns
70
+ }
71
+ elif 'api' in field.metadata:
72
+ # reference information, no matter the type:
73
+ try:
74
+ columns = field.metadata.get('fk').split("|")
75
+ _id, _value = columns
76
+ _fields = {
77
+ "id": _id,
78
+ "value": _value,
79
+ }
80
+ except (TypeError, ValueError):
81
+ _fields = {}
82
+ columns = []
83
+ ref = {
84
+ "api": field.metadata.get('api'),
85
+ **_fields
86
+ }
87
+ return {
88
+ "type": "object",
89
+ "$ref": ref,
90
+ "columns": columns
91
+ }
92
+ return None
93
+
94
+
95
+ class ModelMixin:
96
+ """Interface for shared methods on Model classes.
97
+ """
98
+ def __unicode__(self):
99
+ return str(__class__)
100
+
101
+ def columns(self):
102
+ return self.__columns__
103
+
104
+ @classmethod
105
+ def get_columns(cls):
106
+ return cls.__columns__
107
+
108
+ @classmethod
109
+ def get_column(cls, name: str) -> Field:
110
+ try:
111
+ return cls.__columns__[name]
112
+ except KeyError:
113
+ raise AttributeError(
114
+ f"{cls.__name__} has no column {name}"
115
+ )
116
+
117
+ def get_fields(self):
118
+ return self.__fields__
119
+
120
+ def __getitem__(self, item):
121
+ return getattr(self, item)
122
+
123
+ def reset_values(self):
124
+ try:
125
+ self.__values__ = {}
126
+ except AttributeError:
127
+ pass
128
+
129
+ def old_value(self, name: str) -> Any:
130
+ """
131
+ old_value.
132
+ Get the old value of an attribute.
133
+ Args:
134
+ name (str): name of the attribute.
135
+ Returns:
136
+ Any: value of the attribute.
137
+ """
138
+ try:
139
+ return self.__values__[name]
140
+ except KeyError:
141
+ raise AttributeError(
142
+ f"{self.__class__.__name__} has no attribute {name}"
143
+ )
144
+
145
+ def column(self, name: str) -> Field:
146
+ return self.__columns__[name]
147
+
148
+ def __repr__(self) -> str:
149
+ f_repr = ", ".join(f"{f.name}={getattr(self, f.name)}" for f in fields(self))
150
+ return f"{self.__class__.__name__}({f_repr})"
151
+
152
+ def remove_nulls(self, obj: Any) -> dict[str, Any]:
153
+ """Recursively removes any fields with None values from the given object."""
154
+ if isinstance(obj, list):
155
+ return [self.remove_nulls(item) for item in obj]
156
+ elif isinstance(obj, dict):
157
+ return {
158
+ key: self.remove_nulls(value) for key, value in obj.items()
159
+ if value is not None and value != {}
160
+ }
161
+ else:
162
+ return obj
163
+
164
+ def __convert_enums__(self, obj: Any) -> dict[str, Any]:
165
+ """Recursively converts any Enum values to their value."""
166
+ if isinstance(obj, list):
167
+ return [self.__convert_enums__(item) for item in obj]
168
+ elif isinstance(obj, dict):
169
+ return {
170
+ key: self.__convert_enums__(value) for key, value in obj.items()
171
+ }
172
+ else:
173
+ return obj.value if isinstance(obj, Enum) else obj
174
+
175
+ def to_dict(
176
+ self,
177
+ remove_nulls: bool = False,
178
+ convert_enums: bool = False,
179
+ as_values: bool = False
180
+ ) -> dict[str, Any]:
181
+ if as_values:
182
+ return self.__collapse_as_values__(remove_nulls, convert_enums, as_values)
183
+ d = as_dict(self, dict_factory=dict)
184
+ if convert_enums:
185
+ d = self.__convert_enums__(d)
186
+ if self.Meta.remove_nulls is True or remove_nulls:
187
+ return self.remove_nulls(d)
188
+ # 4) If as_values => convert sub-models to pk-value
189
+ return d
190
+
191
+ def __collapse_as_values__(
192
+ self,
193
+ remove_nulls: bool = False,
194
+ convert_enums: bool = False,
195
+ as_values: bool = False
196
+ ) -> dict[str, Any]:
197
+ """Recursively converts any BaseModel instances to their primary key value."""
198
+ out = {}
199
+ fields = self.columns()
200
+ for name, field in fields.items():
201
+ # datatype = field.type
202
+ value = getattr(self, name)
203
+ if value is None and remove_nulls:
204
+ continue
205
+ if isinstance(value, ModelMixin):
206
+ if as_values:
207
+ out[name] = getattr(value, name)
208
+ else:
209
+ out[name] = value.__collapse_as_values__(
210
+ remove_nulls=remove_nulls,
211
+ convert_enums=convert_enums,
212
+ as_values=as_values
213
+ )
214
+ # if it's a list, might contain submodels or scalars
215
+ elif isinstance(value, list):
216
+ items_out = []
217
+ for item in value:
218
+ if isinstance(item, ModelMixin):
219
+ if as_values:
220
+ items_out.append(getattr(item, name))
221
+ else:
222
+ items_out.append(item.__collapse_as_values__(
223
+ remove_nulls=remove_nulls,
224
+ convert_enums=convert_enums,
225
+ as_values=as_values
226
+ ))
227
+ else:
228
+ items_out.append(item)
229
+ out[name] = items_out
230
+ else:
231
+ out[name] = value
232
+ if convert_enums:
233
+ out = self.__convert_enums__(out)
234
+ return out
235
+
236
+ def json(self, **kwargs):
237
+ encoder = self.__encoder__(**kwargs)
238
+ return encoder(as_dict(self))
239
+
240
+ to_json = json
241
+
242
+ def is_valid(self) -> bool:
243
+ """is_valid.
244
+
245
+ returns True when current Model is valid under datatype validations.
246
+ Returns:
247
+ bool: True if current model is valid.
248
+ """
249
+ return bool(self.__valid__)
250
+
251
+ def get(self, key: str, default=None):
252
+ """
253
+ A dict-like get() method.
254
+ Returns the value of `self.key` if it exists, otherwise returns `default`.
255
+ """
256
+ return getattr(self, key) if hasattr(self, key) else default
257
+
258
+ def _get_meta_value(self, key: str, fallback: Any = None, locale: Any = None):
259
+ value = getattr(self.Meta, key, fallback)
260
+ if locale is not None:
261
+ value = locale(value)
262
+ return value
263
+
264
+ def _get_meta_values(
265
+ self,
266
+ key: dict,
267
+ fallback: Any = None,
268
+ locale: Any = None
269
+ ):
270
+ """
271
+ _get_meta_values.
272
+
273
+ Translates the entire dictionary of Meta values.
274
+ """
275
+ values = getattr(self.Meta, key, fallback)
276
+ if locale is not None:
277
+ for key, val in values.items():
278
+ try:
279
+ values[key] = locale(val)
280
+ except (KeyError, TypeError):
281
+ pass
282
+ return values
283
+
284
+ def _get_metadata(self, field, key: str, locale: Any = None):
285
+ value = field.metadata.get(key, None)
286
+ if locale is not None:
287
+ value = locale(value)
288
+ return value
289
+
290
+ def _get_field_schema(
291
+ self,
292
+ type_info: str,
293
+ field: object,
294
+ description: str,
295
+ locale: Any = None,
296
+ **kwargs
297
+ ) -> dict:
298
+ return {
299
+ "type": type_info,
300
+ "nullable": field.metadata.get('nullable', False),
301
+ "attrs": {
302
+ "placeholder": description,
303
+ "format": field.metadata.get('format', None),
304
+ },
305
+ "readOnly": field.metadata.get('readonly', False),
306
+ **kwargs
307
+ }
308
+
309
+ @classmethod
310
+ def _build_schema_basics(cls, locale: Any = None):
311
+ """Build basic schema metadata such as title, description, etc."""
312
+ # description:
313
+ description = cls._get_meta_value(
314
+ cls,
315
+ 'description',
316
+ fallback=cls.__doc__.strip("\n").strip(),
317
+ locale=locale
318
+ )
319
+ title = cls._get_meta_value(
320
+ cls,
321
+ 'title',
322
+ fallback=cls.__name__,
323
+ locale=locale
324
+ )
325
+ try:
326
+ title = slugify_camelcase(title)
327
+ except Exception:
328
+ pass
329
+ # display_name:
330
+ display_name = cls._get_meta_value(
331
+ cls,
332
+ 'display_name',
333
+ fallback=f"{title}_name".lower(),
334
+ locale=locale
335
+ )
336
+ # Table Name:
337
+ table = cls.Meta.name.lower() if cls.Meta.name else title.lower()
338
+ endpoint = cls.Meta.endpoint
339
+ schema = cls.Meta.schema
340
+ return title, description, display_name, table, endpoint, schema
341
+
342
+ @classmethod
343
+ def _build_settings(cls, locale: Any = None) -> dict:
344
+ """Build the settings part of the schema."""
345
+ # settings:
346
+ settings = cls._get_meta_values(
347
+ cls,
348
+ 'settings',
349
+ fallback={},
350
+ locale=locale
351
+ )
352
+ if not isinstance(settings, dict):
353
+ # Ensure settings is always a dict
354
+ settings = {}
355
+ return {"settings": settings}
356
+
357
+ @classmethod
358
+ def _build_fields(cls, title: str, locale: Any = None) -> dict:
359
+ """Build the fields part of the schema."""
360
+ fields = {}
361
+ required = []
362
+ defs = {}
363
+
364
+ # Get the columns of the Model.
365
+ for name, field in cls.get_columns().items():
366
+ field_schema, field_defs, field_required = cls._process_field_schema(
367
+ name, field, locale, title
368
+ )
369
+ fields[name] = field_schema
370
+ if field_required:
371
+ required.append(name)
372
+ if field_defs:
373
+ defs[name] = field_defs.get('schema')
374
+ return fields, required, defs
375
+
376
+ @classmethod
377
+ def _extract_field_basics(cls, name: str, field: Field, title: str):
378
+ _type = field.type
379
+ type_info = _get_type_info(_type, name, title)
380
+ ref_info = _get_ref_info(_type, field) or {}
381
+ field_defs = {}
382
+
383
+ if 'schema' in ref_info:
384
+ field_defs['schema'] = ref_info.pop('schema', None)
385
+
386
+ return type_info, ref_info, field_defs
387
+
388
+ @classmethod
389
+ def _extract_and_filter_metadata(cls, field: Field, locale: Any):
390
+ """Extract and filter metadata."""
391
+ _metadata = field.metadata.copy()
392
+ minimum = _metadata.pop('min', None)
393
+ maximum = _metadata.pop('max', None)
394
+ secret = _metadata.pop('secret', None)
395
+ custom_endpoint = _metadata.pop('endpoint', None)
396
+
397
+ field_required = field.metadata.get(
398
+ 'required', False
399
+ ) or field.metadata.get('primary', False)
400
+
401
+ ui_objects = {
402
+ k.replace('_', ':'): v for k, v in _metadata.items() if k.startswith('ui_')
403
+ }
404
+ schema_extra = _metadata.pop('schema_extra', {})
405
+
406
+ meta_description = cls._get_metadata(
407
+ cls, field, key='description', locale=locale
408
+ )
409
+
410
+ return (
411
+ _metadata,
412
+ minimum,
413
+ maximum,
414
+ secret,
415
+ custom_endpoint,
416
+ field_required,
417
+ ui_objects,
418
+ schema_extra,
419
+ meta_description
420
+ )
421
+
422
+ @classmethod
423
+ def _apply_extra_metadata(cls, field_schema: dict, _metadata: dict):
424
+ """Move non-rejected metadata keys into the 'attrs' dict."""
425
+ _rejected = [
426
+ 'required', 'nullable', 'primary', 'readonly',
427
+ 'label', 'validator', 'encoder', 'decoder',
428
+ 'default_factory', 'type'
429
+ ]
430
+
431
+ if _meta := {k: v for k, v in _metadata.items() if k not in _rejected}:
432
+ field_schema["attrs"] = {
433
+ **field_schema["attrs"],
434
+ **_meta
435
+ }
436
+
437
+ @classmethod
438
+ def _apply_defaults_and_constraints(
439
+ cls,
440
+ field_schema: dict,
441
+ field: Field,
442
+ secret: Any,
443
+ type_info: str,
444
+ minimum: Any,
445
+ maximum: Any
446
+ ):
447
+ """Handle default values, secret fields, and min/max constraints."""
448
+ if field.default:
449
+ if not isinstance(field.default, _MISSING_TYPE) and not callable(field.default):
450
+ d = field.default
451
+ field_schema['default'] = f"fn:{d!r}" if is_callable(d) else f"{d!s}"
452
+
453
+ if secret is not None:
454
+ field_schema['secret'] = secret
455
+
456
+ # Handle length/size constraints
457
+ if type_info == 'string':
458
+ if minimum is not None:
459
+ field_schema['minLength'] = minimum
460
+ if maximum is not None:
461
+ field_schema['maxLength'] = maximum
462
+ else:
463
+ if minimum is not None:
464
+ field_schema['minimum'] = minimum
465
+ if maximum is not None:
466
+ field_schema['maximum'] = maximum
467
+
468
+ @classmethod
469
+ def _process_field_schema(
470
+ cls,
471
+ name: str,
472
+ field: Field,
473
+ locale: Any,
474
+ title: str
475
+ ) -> tuple:
476
+ """Process the schema for a single field."""
477
+ # Get the field type and description.
478
+
479
+ type_info, ref_info, field_defs = cls._extract_field_basics(name, field, title)
480
+
481
+ # Extract and handle metadata
482
+ (
483
+ _metadata,
484
+ minimum,
485
+ maximum,
486
+ secret,
487
+ custom_endpoint,
488
+ field_required,
489
+ ui_objects,
490
+ schema_extra,
491
+ meta_description
492
+ ) = cls._extract_and_filter_metadata(
493
+ field, locale
494
+ )
495
+
496
+ if 'schema' in ref_info:
497
+ field_defs['schema'] = ref_info.pop('schema', None)
498
+
499
+ # Build the basic field schema
500
+ field_schema = cls._get_field_schema(
501
+ cls,
502
+ type_info,
503
+ field,
504
+ description=meta_description,
505
+ locale=locale,
506
+ **ui_objects,
507
+ **schema_extra,
508
+ **ref_info
509
+ )
510
+
511
+ # Handle primary/required keys
512
+ if field.metadata.get('primary', False) is True:
513
+ field_schema["primary_key"] = True
514
+ if field_required:
515
+ field_schema["required"] = True
516
+
517
+ # Add label and description if available
518
+ label = cls._get_metadata(cls, field, 'label', locale=locale)
519
+ if label:
520
+ field_schema["label"] = label
521
+ if meta_description:
522
+ field_schema["description"] = meta_description
523
+
524
+ # Add custom endpoint
525
+ if custom_endpoint:
526
+ field_schema["endpoint"] = custom_endpoint
527
+
528
+ # Handle write_only, pattern, visible attributes
529
+ if 'write_only' in field.metadata:
530
+ field_schema["writeOnly"] = _metadata.pop('write_only', False)
531
+
532
+ if 'pattern' in field.metadata:
533
+ field_schema["attrs"]["pattern"] = _metadata.pop('pattern')
534
+
535
+ if field.repr is False:
536
+ field_schema["attrs"]["visible"] = False
537
+
538
+ # Remove some rejected keys and move others into attrs
539
+ cls._apply_extra_metadata(field_schema, _metadata)
540
+
541
+ # Handle default, secret, and constraints
542
+ cls._apply_defaults_and_constraints(
543
+ field_schema,
544
+ field,
545
+ secret,
546
+ type_info,
547
+ minimum,
548
+ maximum
549
+ )
550
+
551
+ return field_schema, field_defs, field_required
552
+
553
+ @classmethod
554
+ def schema(cls, as_dict=False, locale: Any = None):
555
+ """
556
+ Convert the Model to a JSON-Schema representation.
557
+
558
+ This method generates a JSON-Schema that describes the structure and constraints
559
+ of the Model. It includes information about fields, their types,
560
+ validation rules, and other metadata.
561
+
562
+ Args:
563
+ as_dict (bool, optional): If True,
564
+ returns the schema as a Python dictionary.
565
+ If False, returns the schema as a JSON-encoded string.
566
+ Defaults to False.
567
+ locale (Any, optional):
568
+ The locale to use for internationalization of schema
569
+ elements like descriptions and labels. Defaults to None.
570
+
571
+ Returns:
572
+ Union[dict, str]:
573
+ The JSON-Schema representation of the Model. If as_dict is True,
574
+ returns a Python dictionary. Otherwise, returns a JSON-encoded string.
575
+
576
+ Note:
577
+ This method caches the computed schema in the __computed_schema__ attribute
578
+ of the class for subsequent calls.
579
+ """
580
+ # Check if schema is already computed and cached.
581
+ if hasattr(cls, '__computed_schema__'):
582
+ return cls.__computed_schema__ if as_dict else json_encoder(
583
+ cls.__computed_schema__
584
+ )
585
+
586
+ # Build basic schema attributes (title, description, display_name, etc.)
587
+ title, description, display_name, table, endpoint, schema = cls._build_schema_basics(locale) # pylint: disable=C0301 # noqa
588
+ settings = cls._build_settings(locale)
589
+ endpoint_kwargs = {"endpoint": endpoint} if endpoint else {}
590
+
591
+ # Build the fields part of the schema.
592
+ fields, required, defs = cls._build_fields(title, locale)
593
+
594
+ base_schema = {
595
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
596
+ "$id": f"/schemas/{table}",
597
+ **endpoint_kwargs,
598
+ **settings,
599
+ "additionalProperties": cls.Meta.strict,
600
+ "title": title,
601
+ "description": description,
602
+ "type": "object",
603
+ "table": table,
604
+ "schema": schema,
605
+ "properties": fields,
606
+ "required": required,
607
+ "display_name": display_name,
608
+ }
609
+
610
+ if defs:
611
+ base_schema["$defs"] = defs
612
+
613
+ # Cache the computed schema for subsequent calls
614
+ cls.__computed_schema__ = base_schema
615
+
616
+ return base_schema if as_dict else json_encoder(base_schema)
617
+
618
+ def as_schema(self, top_level: bool = True) -> dict:
619
+ """as_schema.
620
+ Convert the Model instance to a JSON-LD schema representation.
621
+ Args:
622
+ top_level (bool, optional): If True, adds the @context to the schema.
623
+ Returns:
624
+ dict: JSON-LD schema representation of the Model instance.
625
+ """
626
+ data = {}
627
+ # If top_level, add @context
628
+ if top_level:
629
+ data["@context"] = "https://schema.org/"
630
+
631
+ # Determine the schema @type
632
+ schema_type = getattr(self.Meta, 'schema_type', self.__class__.__name__)
633
+ data["@type"] = schema_type
634
+
635
+ for field_name, field_obj in self.__columns__.items():
636
+ # Skip internal or error fields
637
+ if field_name.startswith('__') or field_name == '__errors__':
638
+ continue
639
+
640
+ value = getattr(self, field_name)
641
+ if isinstance(value, ModelMixin):
642
+ data[field_name] = value.as_schema(top_level=False)
643
+ else:
644
+ data[field_name] = value
645
+
646
+ return data
647
+
648
+ @classmethod
649
+ def make_model(cls, name: str, schema: str = "public", fields: list = None):
650
+ parent = inspect.getmro(cls)
651
+ obj = make_dataclass(name, fields, bases=(parent[0],))
652
+ m = Meta()
653
+ m.name = name
654
+ m.schema = schema
655
+ obj.Meta = m
656
+ return obj
657
+
658
+ @classmethod
659
+ def from_json(cls, obj: str, **kwargs) -> dataclass:
660
+ try:
661
+ decoder = cls.__encoder__(**kwargs)
662
+ decoded = decoder.loads(obj)
663
+ return cls(**decoded)
664
+ except ValueError as e:
665
+ raise RuntimeError(
666
+ "DataModel: Invalid string (JSON) data for decoding: {e}"
667
+ ) from e
668
+
669
+ @classmethod
670
+ def from_dict(cls, obj: dict) -> dataclass:
671
+ try:
672
+ return cls(**obj)
673
+ except ValueError as e:
674
+ raise RuntimeError(
675
+ "DataModel: Invalid Dictionary data for decoding: {e}"
676
+ ) from e
677
+
678
+ @classmethod
679
+ def model(cls, dialect: str = "json", **kwargs) -> Any:
680
+ """model.
681
+
682
+ Return the json-version of current Model.
683
+ Returns:
684
+ str: string (json) version of model.
685
+ """
686
+ if hasattr(cls, '__computed_model__'):
687
+ return cls.__computed_model__
688
+ result = None
689
+ clsname = cls.__name__
690
+ schema = cls.Meta.schema
691
+ table = cls.Meta.name or clsname.lower()
692
+ columns = cls.columns(cls).items()
693
+ if dialect == 'json':
694
+ cols = {}
695
+ for _, field in columns:
696
+ key = field.name
697
+ _type = field.type
698
+ if _type.__module__ == 'typing':
699
+ # TODO: discover real value of typing
700
+ if _type._name == 'List':
701
+ t = 'array'
702
+ elif _type._name == 'Dict':
703
+ t = 'object'
704
+ else:
705
+ try:
706
+ t = _type.__args__[0]
707
+ t = t.__name__
708
+ except (AttributeError, ValueError):
709
+ t = 'object'
710
+ else:
711
+ try:
712
+ t = JSON_TYPES[_type]
713
+ except KeyError:
714
+ t = 'object'
715
+ cols[key] = {"name": key, "type": t}
716
+ doc = {
717
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
718
+ "$id": f"/schemas/{table}",
719
+ "name": clsname,
720
+ "description": cls.__doc__.strip("\n").strip(),
721
+ "additionalProperties": False,
722
+ "table": table,
723
+ "schema": schema,
724
+ "type": "object",
725
+ "properties": cols,
726
+ }
727
+ encoder = cls.__encoder__(**kwargs)
728
+ result = encoder.dumps(doc, option=OPT_INDENT_2)
729
+ cls.__computed_model__ = result
730
+ return result
731
+
732
+ @classmethod
733
+ def sample(cls) -> dict:
734
+ """sample.
735
+
736
+ Get a dict (JSON) sample of this datamodel, based on default values.
737
+
738
+ Returns:
739
+ dict: _description_
740
+ """
741
+ columns = cls.get_columns().items()
742
+ _fields = {}
743
+ required = []
744
+ for name, f in columns:
745
+ if f.repr is False:
746
+ continue
747
+ _fields[name] = f.default
748
+ try:
749
+ if f.metadata["required"] is True:
750
+ required.append(name)
751
+ except KeyError:
752
+ pass
753
+ return {
754
+ "properties": _fields,
755
+ "required": required
756
+ }
757
+
758
+ @classmethod
759
+ def from_jsonld(cls, data: Dict[str, Any]) -> "ModelMixin":
760
+ """
761
+ Create a model instance from a JSON-LD dictionary.
762
+
763
+ Ignores @context and @type; attempts to parse all other top-level fields
764
+ into the model’s constructor. If the JSON-LD has nested objects that
765
+ correspond to other BaseModel fields, you may need additional logic
766
+ to instantiate sub-models.
767
+ """
768
+ if not isinstance(data, dict):
769
+ raise ValueError("JSON-LD input must be a dictionary.")
770
+ # If present, remove the JSON-LD keys that are not actual model fields
771
+ data.pop("@context", None)
772
+ data.pop("@type", None)
773
+
774
+
775
+ class Model(ModelMixin, metaclass=ModelMeta):
776
+ """Model.
777
+
778
+ Basic dataclass-based Model.
779
+ """
780
+ Meta = Meta
781
+
782
+ def __post_init__(self) -> None:
783
+ """
784
+ Post init method.
785
+ Useful for making Post-validations of Model.
786
+ """
787
+ self.__initialised__ = True