dhi 1.1.1__cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.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.
dhi/model.py ADDED
@@ -0,0 +1,658 @@
1
+ """
2
+ BaseModel implementation for dhi - Pydantic v2 compatible.
3
+
4
+ Provides a lightweight, high-performance BaseModel that validates data
5
+ on instantiation using type annotations and constraints.
6
+
7
+ Example:
8
+ from typing import Annotated
9
+ from dhi import BaseModel, Field, PositiveInt, EmailStr
10
+
11
+ class User(BaseModel):
12
+ name: Annotated[str, Field(min_length=1, max_length=100)]
13
+ age: PositiveInt
14
+ email: EmailStr
15
+ score: Annotated[float, Field(ge=0, le=100)] = 0.0
16
+
17
+ user = User(name="Alice", age=25, email="alice@example.com")
18
+ print(user.model_dump())
19
+ """
20
+
21
+ import re
22
+ import math
23
+ import copy
24
+ from typing import (
25
+ Any, Dict, List, Optional, Set, Type, Tuple, Union,
26
+ get_type_hints,
27
+ )
28
+
29
+ try:
30
+ from typing import get_args, get_origin, Annotated
31
+ except ImportError:
32
+ from typing_extensions import get_args, get_origin, Annotated
33
+
34
+ from .constraints import (
35
+ Gt, Ge, Lt, Le, MultipleOf,
36
+ MinLength, MaxLength, Pattern,
37
+ Strict, StripWhitespace, ToLower, ToUpper,
38
+ AllowInfNan, MaxDigits, DecimalPlaces, UniqueItems,
39
+ StringConstraints,
40
+ )
41
+ from .fields import FieldInfo, Field, _MISSING
42
+ from .validator import ValidationError, ValidationErrors
43
+
44
+
45
+ # Cache for compiled validators per class
46
+ _CLASS_VALIDATORS_CACHE: Dict[type, Dict[str, Any]] = {}
47
+
48
+
49
+ def _extract_constraints(annotation: Any) -> Tuple[Type, List[Any]]:
50
+ """Extract base type and constraint metadata from an annotation.
51
+
52
+ Handles:
53
+ - Plain types: int, str, float
54
+ - Annotated types: Annotated[int, Gt(gt=0), Le(le=100)]
55
+ - FieldInfo in Annotated: Annotated[str, Field(min_length=1)]
56
+ """
57
+ origin = get_origin(annotation)
58
+ if origin is Annotated:
59
+ args = get_args(annotation)
60
+ base_type = args[0]
61
+ constraints = list(args[1:])
62
+ # Recursively unwrap nested Annotated (e.g., PositiveInt used in Annotated)
63
+ nested_origin = get_origin(base_type)
64
+ if nested_origin is Annotated:
65
+ nested_args = get_args(base_type)
66
+ base_type = nested_args[0]
67
+ constraints = list(nested_args[1:]) + constraints
68
+ return base_type, constraints
69
+ return annotation, []
70
+
71
+
72
+ def _build_validator(field_name: str, base_type: Type, constraints: List[Any]) -> Any:
73
+ """Build a compiled validator function for a field.
74
+
75
+ Returns a function that takes a value and returns the validated/transformed value,
76
+ or raises ValidationError.
77
+ """
78
+ # Collect all constraints from both individual metadata and FieldInfo objects
79
+ gt = ge = lt = le = multiple_of = None
80
+ min_length = max_length = None
81
+ pattern_str = None
82
+ strict = False
83
+ strip_whitespace = to_lower = to_upper = False
84
+ allow_inf_nan = True
85
+ max_digits = decimal_places = None
86
+ unique_items = False
87
+ custom_validators: List[Any] = []
88
+
89
+ for constraint in constraints:
90
+ if isinstance(constraint, Gt):
91
+ gt = constraint.gt
92
+ elif isinstance(constraint, Ge):
93
+ ge = constraint.ge
94
+ elif isinstance(constraint, Lt):
95
+ lt = constraint.lt
96
+ elif isinstance(constraint, Le):
97
+ le = constraint.le
98
+ elif isinstance(constraint, MultipleOf):
99
+ multiple_of = constraint.multiple_of
100
+ elif isinstance(constraint, MinLength):
101
+ min_length = constraint.min_length
102
+ elif isinstance(constraint, MaxLength):
103
+ max_length = constraint.max_length
104
+ elif isinstance(constraint, Pattern):
105
+ pattern_str = constraint.pattern
106
+ elif isinstance(constraint, Strict):
107
+ strict = constraint.strict
108
+ elif isinstance(constraint, StripWhitespace):
109
+ strip_whitespace = constraint.strip_whitespace
110
+ elif isinstance(constraint, ToLower):
111
+ to_lower = constraint.to_lower
112
+ elif isinstance(constraint, ToUpper):
113
+ to_upper = constraint.to_upper
114
+ elif isinstance(constraint, AllowInfNan):
115
+ allow_inf_nan = constraint.allow_inf_nan
116
+ elif isinstance(constraint, MaxDigits):
117
+ max_digits = constraint.max_digits
118
+ elif isinstance(constraint, DecimalPlaces):
119
+ decimal_places = constraint.decimal_places
120
+ elif isinstance(constraint, UniqueItems):
121
+ unique_items = constraint.unique_items
122
+ elif isinstance(constraint, StringConstraints):
123
+ # Unpack compound constraints
124
+ if constraint.min_length is not None:
125
+ min_length = constraint.min_length
126
+ if constraint.max_length is not None:
127
+ max_length = constraint.max_length
128
+ if constraint.pattern is not None:
129
+ pattern_str = constraint.pattern
130
+ if constraint.strip_whitespace:
131
+ strip_whitespace = True
132
+ if constraint.to_lower:
133
+ to_lower = True
134
+ if constraint.to_upper:
135
+ to_upper = True
136
+ if constraint.strict:
137
+ strict = True
138
+ elif isinstance(constraint, FieldInfo):
139
+ # Extract constraints from FieldInfo
140
+ if constraint.gt is not None:
141
+ gt = constraint.gt
142
+ if constraint.ge is not None:
143
+ ge = constraint.ge
144
+ if constraint.lt is not None:
145
+ lt = constraint.lt
146
+ if constraint.le is not None:
147
+ le = constraint.le
148
+ if constraint.multiple_of is not None:
149
+ multiple_of = constraint.multiple_of
150
+ if constraint.min_length is not None:
151
+ min_length = constraint.min_length
152
+ if constraint.max_length is not None:
153
+ max_length = constraint.max_length
154
+ if constraint.pattern is not None:
155
+ pattern_str = constraint.pattern
156
+ if constraint.strict:
157
+ strict = True
158
+ if constraint.strip_whitespace:
159
+ strip_whitespace = True
160
+ if constraint.to_lower:
161
+ to_lower = True
162
+ if constraint.to_upper:
163
+ to_upper = True
164
+ if constraint.allow_inf_nan is not None:
165
+ allow_inf_nan = constraint.allow_inf_nan
166
+ if constraint.max_digits is not None:
167
+ max_digits = constraint.max_digits
168
+ if constraint.decimal_places is not None:
169
+ decimal_places = constraint.decimal_places
170
+ if constraint.unique_items:
171
+ unique_items = True
172
+ elif hasattr(constraint, 'validate') and callable(constraint.validate):
173
+ # Custom validator object (e.g., _EmailValidator, _UrlValidator, etc.)
174
+ custom_validators.append(constraint)
175
+ elif callable(constraint):
176
+ custom_validators.append(constraint)
177
+
178
+ # Pre-compile pattern if present
179
+ compiled_pattern = re.compile(pattern_str) if pattern_str else None
180
+
181
+ # Determine the expected Python type for type checking
182
+ # Handle generic types (List[int] -> list, Set[str] -> set, etc.)
183
+ check_type = base_type
184
+ type_origin = get_origin(base_type)
185
+ if type_origin is not None:
186
+ check_type = type_origin
187
+
188
+ def validator(value: Any) -> Any:
189
+ # Type checking
190
+ if strict:
191
+ if type(value) is not check_type:
192
+ raise ValidationError(
193
+ field_name,
194
+ f"Expected exactly {check_type.__name__}, got {type(value).__name__}"
195
+ )
196
+ else:
197
+ # Coerce compatible types
198
+ if check_type in (int, float) and not isinstance(value, check_type):
199
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
200
+ try:
201
+ value = check_type(value)
202
+ except (ValueError, TypeError, OverflowError):
203
+ raise ValidationError(
204
+ field_name,
205
+ f"Cannot convert {type(value).__name__} to {check_type.__name__}"
206
+ )
207
+ else:
208
+ raise ValidationError(
209
+ field_name,
210
+ f"Expected {check_type.__name__}, got {type(value).__name__}"
211
+ )
212
+ elif check_type is str and not isinstance(value, str):
213
+ raise ValidationError(
214
+ field_name,
215
+ f"Expected str, got {type(value).__name__}"
216
+ )
217
+ elif check_type is bytes and not isinstance(value, bytes):
218
+ raise ValidationError(
219
+ field_name,
220
+ f"Expected bytes, got {type(value).__name__}"
221
+ )
222
+ elif check_type is bool and not isinstance(value, bool):
223
+ raise ValidationError(
224
+ field_name,
225
+ f"Expected bool, got {type(value).__name__}"
226
+ )
227
+ elif check_type in (list, set, frozenset) and not isinstance(value, check_type):
228
+ raise ValidationError(
229
+ field_name,
230
+ f"Expected {check_type.__name__}, got {type(value).__name__}"
231
+ )
232
+
233
+ # String transformations (before validation)
234
+ if isinstance(value, str):
235
+ if strip_whitespace:
236
+ value = value.strip()
237
+ if to_lower:
238
+ value = value.lower()
239
+ if to_upper:
240
+ value = value.upper()
241
+
242
+ # Numeric constraints
243
+ if gt is not None and value <= gt:
244
+ raise ValidationError(field_name, f"Value must be > {gt}, got {value}")
245
+ if ge is not None and value < ge:
246
+ raise ValidationError(field_name, f"Value must be >= {ge}, got {value}")
247
+ if lt is not None and value >= lt:
248
+ raise ValidationError(field_name, f"Value must be < {lt}, got {value}")
249
+ if le is not None and value > le:
250
+ raise ValidationError(field_name, f"Value must be <= {le}, got {value}")
251
+ if multiple_of is not None and value % multiple_of != 0:
252
+ raise ValidationError(field_name, f"Value must be a multiple of {multiple_of}, got {value}")
253
+
254
+ # Float-specific constraints
255
+ if not allow_inf_nan and isinstance(value, float):
256
+ if math.isinf(value) or math.isnan(value):
257
+ raise ValidationError(field_name, f"Value must be finite, got {value}")
258
+
259
+ # Length constraints (strings, bytes, collections)
260
+ if min_length is not None or max_length is not None:
261
+ length = len(value)
262
+ if min_length is not None and length < min_length:
263
+ raise ValidationError(
264
+ field_name,
265
+ f"Length must be >= {min_length}, got {length}"
266
+ )
267
+ if max_length is not None and length > max_length:
268
+ raise ValidationError(
269
+ field_name,
270
+ f"Length must be <= {max_length}, got {length}"
271
+ )
272
+
273
+ # Pattern constraint
274
+ if compiled_pattern is not None and isinstance(value, str):
275
+ if not compiled_pattern.match(value):
276
+ raise ValidationError(
277
+ field_name,
278
+ f"String does not match pattern '{pattern_str}'"
279
+ )
280
+
281
+ # Decimal constraints
282
+ if max_digits is not None or decimal_places is not None:
283
+ from decimal import Decimal
284
+ if isinstance(value, Decimal):
285
+ sign, digits, exp = value.as_tuple()
286
+ num_digits = len(digits)
287
+ if max_digits is not None and num_digits > max_digits:
288
+ raise ValidationError(
289
+ field_name,
290
+ f"Decimal must have at most {max_digits} digits, got {num_digits}"
291
+ )
292
+ if decimal_places is not None:
293
+ actual_places = -exp if exp < 0 else 0
294
+ if actual_places > decimal_places:
295
+ raise ValidationError(
296
+ field_name,
297
+ f"Decimal must have at most {decimal_places} decimal places, got {actual_places}"
298
+ )
299
+
300
+ # Unique items constraint
301
+ if unique_items and isinstance(value, list):
302
+ seen = set()
303
+ for item in value:
304
+ item_key = repr(item) # Use repr for unhashable items
305
+ if item_key in seen:
306
+ raise ValidationError(
307
+ field_name,
308
+ f"List items must be unique, found duplicate: {item!r}"
309
+ )
310
+ seen.add(item_key)
311
+
312
+ # Custom validators (objects with .validate() or callables)
313
+ for custom_val in custom_validators:
314
+ if hasattr(custom_val, 'validate'):
315
+ value = custom_val.validate(value, field_name)
316
+ else:
317
+ value = custom_val(value)
318
+
319
+ return value
320
+
321
+ return validator
322
+
323
+
324
+ class _ModelMeta(type):
325
+ """Metaclass for BaseModel that compiles validators at class creation."""
326
+
327
+ def __new__(mcs, name: str, bases: tuple, namespace: dict) -> type:
328
+ cls = super().__new__(mcs, name, bases, namespace)
329
+
330
+ if name == 'BaseModel':
331
+ return cls
332
+
333
+ # Get type hints including Annotated metadata
334
+ try:
335
+ hints = get_type_hints(cls, include_extras=True)
336
+ except Exception:
337
+ hints = {}
338
+
339
+ # Build field info and validators
340
+ fields: Dict[str, Dict[str, Any]] = {}
341
+ validators: Dict[str, Any] = {}
342
+
343
+ for field_name, annotation in hints.items():
344
+ if field_name.startswith('_'):
345
+ continue
346
+
347
+ base_type, constraints = _extract_constraints(annotation)
348
+
349
+ # Check for class-level default
350
+ default = namespace.get(field_name, _MISSING)
351
+ default_factory = None
352
+
353
+ # Check if any constraint is a FieldInfo with a default
354
+ for c in constraints:
355
+ if isinstance(c, FieldInfo):
356
+ if c.default is not _MISSING:
357
+ default = c.default
358
+ break
359
+ if c.default_factory is not None:
360
+ default_factory = c.default_factory
361
+ default = default_factory # Mark as not required
362
+ break
363
+
364
+ fields[field_name] = {
365
+ 'annotation': annotation,
366
+ 'base_type': base_type,
367
+ 'constraints': constraints,
368
+ 'default': default,
369
+ 'default_factory': default_factory,
370
+ 'required': default is _MISSING and default_factory is None,
371
+ }
372
+ validators[field_name] = _build_validator(field_name, base_type, constraints)
373
+
374
+ cls.__dhi_fields__ = fields
375
+ cls.__dhi_validators__ = validators
376
+ cls.__dhi_field_names__ = list(fields.keys())
377
+
378
+ return cls
379
+
380
+
381
+ class BaseModel(metaclass=_ModelMeta):
382
+ """High-performance validated model - Pydantic v2 compatible API.
383
+
384
+ Define models with type annotations and constraints. Data is validated
385
+ on instantiation.
386
+
387
+ Example:
388
+ from typing import Annotated
389
+ from dhi import BaseModel, Field, PositiveInt
390
+
391
+ class User(BaseModel):
392
+ name: Annotated[str, Field(min_length=1, max_length=100)]
393
+ age: PositiveInt
394
+ email: str
395
+ score: Annotated[float, Field(ge=0, le=100)] = 0.0
396
+
397
+ user = User(name="Alice", age=25, email="alice@example.com")
398
+ assert user.name == "Alice"
399
+ assert user.model_dump() == {"name": "Alice", "age": 25, "email": "alice@example.com", "score": 0.0}
400
+ """
401
+
402
+ # These are set by the metaclass
403
+ __dhi_fields__: Dict[str, Dict[str, Any]]
404
+ __dhi_validators__: Dict[str, Any]
405
+ __dhi_field_names__: List[str]
406
+
407
+ def __init__(self, **kwargs: Any) -> None:
408
+ errors: List[ValidationError] = []
409
+
410
+ # Run field validators and model validators
411
+ field_validators = getattr(self.__class__, '__dhi_field_validator_funcs__', {})
412
+ model_validators_before = getattr(self.__class__, '__dhi_model_validators_before__', [])
413
+ model_validators_after = getattr(self.__class__, '__dhi_model_validators_after__', [])
414
+
415
+ # Run 'before' model validators
416
+ for mv in model_validators_before:
417
+ kwargs = mv(kwargs)
418
+
419
+ for field_name in self.__dhi_field_names__:
420
+ field_info = self.__dhi_fields__[field_name]
421
+ alias = None
422
+ # Check for alias in constraints
423
+ for c in field_info['constraints']:
424
+ if isinstance(c, FieldInfo) and c.alias:
425
+ alias = c.alias
426
+ break
427
+
428
+ # Get value from kwargs (checking alias first)
429
+ if alias and alias in kwargs:
430
+ value = kwargs[alias]
431
+ elif field_name in kwargs:
432
+ value = kwargs[field_name]
433
+ elif not field_info['required']:
434
+ factory = field_info.get('default_factory')
435
+ if factory is not None:
436
+ value = factory()
437
+ else:
438
+ default = field_info['default']
439
+ value = copy.deepcopy(default) if isinstance(default, (list, dict, set)) else default
440
+ object.__setattr__(self, field_name, value)
441
+ continue
442
+ else:
443
+ errors.append(ValidationError(field_name, "Field required"))
444
+ continue
445
+
446
+ try:
447
+ validator = self.__dhi_validators__[field_name]
448
+ validated = validator(value)
449
+
450
+ # Run field-specific validators
451
+ if field_name in field_validators:
452
+ for fv in field_validators[field_name]:
453
+ validated = fv(validated)
454
+
455
+ object.__setattr__(self, field_name, validated)
456
+ except ValidationError as e:
457
+ errors.append(e)
458
+
459
+ if errors:
460
+ raise ValidationErrors(errors)
461
+
462
+ # Run 'after' model validators
463
+ for mv in model_validators_after:
464
+ mv(self)
465
+
466
+ def __init_subclass__(cls, **kwargs: Any) -> None:
467
+ super().__init_subclass__(**kwargs)
468
+ # Collect field_validator and model_validator decorated methods
469
+ field_validator_funcs: Dict[str, List] = {}
470
+ model_validators_before: List = []
471
+ model_validators_after: List = []
472
+
473
+ # Check class __dict__ directly to find decorated methods
474
+ # This handles @classmethod, @staticmethod wrapping properly
475
+ for attr_name, raw_attr in cls.__dict__.items():
476
+ if attr_name.startswith('__'):
477
+ continue
478
+
479
+ # Check both the raw attribute and unwrapped function for validator markers
480
+ # Decorators may set attrs on either the wrapper or the inner function
481
+ candidates = [raw_attr]
482
+ if isinstance(raw_attr, (classmethod, staticmethod)):
483
+ candidates.append(raw_attr.__func__)
484
+
485
+ validator_fields = None
486
+ model_validator_flag = False
487
+ validator_mode = 'after'
488
+
489
+ for candidate in candidates:
490
+ if hasattr(candidate, '__validator_fields__'):
491
+ validator_fields = candidate.__validator_fields__
492
+ validator_mode = getattr(candidate, '__validator_mode__', 'after')
493
+ break
494
+ if hasattr(candidate, '__model_validator__'):
495
+ model_validator_flag = True
496
+ validator_mode = getattr(candidate, '__validator_mode__', 'after')
497
+ break
498
+
499
+ if validator_fields:
500
+ bound = getattr(cls, attr_name)
501
+ for field_name in validator_fields:
502
+ if field_name not in field_validator_funcs:
503
+ field_validator_funcs[field_name] = []
504
+ field_validator_funcs[field_name].append(bound)
505
+
506
+ if model_validator_flag:
507
+ bound = getattr(cls, attr_name)
508
+ if validator_mode == 'before':
509
+ model_validators_before.append(bound)
510
+ else:
511
+ model_validators_after.append(bound)
512
+
513
+ cls.__dhi_field_validator_funcs__ = field_validator_funcs
514
+ cls.__dhi_model_validators_before__ = model_validators_before
515
+ cls.__dhi_model_validators_after__ = model_validators_after
516
+
517
+ @classmethod
518
+ def model_validate(cls, data: Dict[str, Any]) -> "BaseModel":
519
+ """Validate a dictionary and create a model instance.
520
+
521
+ Matches Pydantic's model_validate() classmethod.
522
+ """
523
+ return cls(**data)
524
+
525
+ def model_dump(self, *, exclude: Optional[Set[str]] = None, include: Optional[Set[str]] = None) -> Dict[str, Any]:
526
+ """Convert model to dictionary.
527
+
528
+ Matches Pydantic's model_dump() method.
529
+ """
530
+ result = {}
531
+ for field_name in self.__dhi_field_names__:
532
+ if exclude and field_name in exclude:
533
+ continue
534
+ if include and field_name not in include:
535
+ continue
536
+ result[field_name] = getattr(self, field_name)
537
+ return result
538
+
539
+ def model_dump_json(self) -> str:
540
+ """Convert model to JSON string."""
541
+ import json
542
+ return json.dumps(self.model_dump())
543
+
544
+ @classmethod
545
+ def model_json_schema(cls) -> Dict[str, Any]:
546
+ """Generate JSON Schema for this model.
547
+
548
+ Matches Pydantic's model_json_schema() classmethod.
549
+ """
550
+ schema: Dict[str, Any] = {
551
+ "title": cls.__name__,
552
+ "type": "object",
553
+ "properties": {},
554
+ "required": [],
555
+ }
556
+
557
+ type_map = {
558
+ int: "integer",
559
+ float: "number",
560
+ str: "string",
561
+ bool: "boolean",
562
+ bytes: "string",
563
+ }
564
+
565
+ for field_name, field_info in cls.__dhi_fields__.items():
566
+ base_type = field_info['base_type']
567
+ constraints = field_info['constraints']
568
+
569
+ prop: Dict[str, Any] = {}
570
+
571
+ # Base type
572
+ json_type = type_map.get(base_type, "string")
573
+ prop["type"] = json_type
574
+
575
+ # Apply constraints to schema
576
+ for c in constraints:
577
+ if isinstance(c, Gt):
578
+ prop["exclusiveMinimum"] = c.gt
579
+ elif isinstance(c, Ge):
580
+ prop["minimum"] = c.ge
581
+ elif isinstance(c, Lt):
582
+ prop["exclusiveMaximum"] = c.lt
583
+ elif isinstance(c, Le):
584
+ prop["maximum"] = c.le
585
+ elif isinstance(c, MultipleOf):
586
+ prop["multipleOf"] = c.multiple_of
587
+ elif isinstance(c, MinLength):
588
+ prop["minLength"] = c.min_length
589
+ elif isinstance(c, MaxLength):
590
+ prop["maxLength"] = c.max_length
591
+ elif isinstance(c, Pattern):
592
+ prop["pattern"] = c.pattern
593
+ elif isinstance(c, FieldInfo):
594
+ if c.gt is not None:
595
+ prop["exclusiveMinimum"] = c.gt
596
+ if c.ge is not None:
597
+ prop["minimum"] = c.ge
598
+ if c.lt is not None:
599
+ prop["exclusiveMaximum"] = c.lt
600
+ if c.le is not None:
601
+ prop["maximum"] = c.le
602
+ if c.multiple_of is not None:
603
+ prop["multipleOf"] = c.multiple_of
604
+ if c.min_length is not None:
605
+ prop["minLength"] = c.min_length
606
+ if c.max_length is not None:
607
+ prop["maxLength"] = c.max_length
608
+ if c.pattern is not None:
609
+ prop["pattern"] = c.pattern
610
+ if c.title:
611
+ prop["title"] = c.title
612
+ if c.description:
613
+ prop["description"] = c.description
614
+ if c.examples:
615
+ prop["examples"] = c.examples
616
+
617
+ # Default value
618
+ if not field_info['required']:
619
+ prop["default"] = field_info['default']
620
+
621
+ schema["properties"][field_name] = prop
622
+
623
+ if field_info['required']:
624
+ schema["required"].append(field_name)
625
+
626
+ return schema
627
+
628
+ def model_copy(self, *, update: Optional[Dict[str, Any]] = None) -> "BaseModel":
629
+ """Create a copy of the model with optional field updates.
630
+
631
+ Matches Pydantic's model_copy() method.
632
+ """
633
+ data = self.model_dump()
634
+ if update:
635
+ data.update(update)
636
+ return self.__class__(**data)
637
+
638
+ def __repr__(self) -> str:
639
+ fields = ", ".join(
640
+ f"{name}={getattr(self, name)!r}"
641
+ for name in self.__dhi_field_names__
642
+ if hasattr(self, name)
643
+ )
644
+ return f"{self.__class__.__name__}({fields})"
645
+
646
+ def __str__(self) -> str:
647
+ return self.__repr__()
648
+
649
+ def __eq__(self, other: object) -> bool:
650
+ if not isinstance(other, self.__class__):
651
+ return NotImplemented
652
+ return self.model_dump() == other.model_dump()
653
+
654
+ def __hash__(self) -> int:
655
+ return hash(tuple(sorted(self.model_dump().items())))
656
+
657
+
658
+ __all__ = ["BaseModel"]