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