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/__init__.py +173 -0
- dhi/_dhi_native.cpython-313-aarch64-linux-gnu.so +0 -0
- dhi/_native.c +379 -0
- dhi/batch.py +236 -0
- dhi/constraints.py +358 -0
- dhi/datetime_types.py +108 -0
- dhi/fields.py +187 -0
- dhi/functional_validators.py +108 -0
- dhi/libsatya.so +0 -0
- dhi/model.py +658 -0
- dhi/networks.py +290 -0
- dhi/secret.py +105 -0
- dhi/special_types.py +359 -0
- dhi/types.py +345 -0
- dhi/validator.py +212 -0
- dhi-1.1.1.dist-info/METADATA +115 -0
- dhi-1.1.1.dist-info/RECORD +21 -0
- dhi-1.1.1.dist-info/WHEEL +6 -0
- dhi-1.1.1.dist-info/licenses/LICENSE +21 -0
- dhi-1.1.1.dist-info/top_level.txt +1 -0
- dhi.libs/libsatya-a22d98f4.so +0 -0
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"]
|