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/batch.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-performance batch validation API
|
|
3
|
+
|
|
4
|
+
This module provides batch validation functions that minimize FFI overhead
|
|
5
|
+
by validating multiple items in a single call to the native Zig library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Dict, Any, Tuple
|
|
9
|
+
import ctypes
|
|
10
|
+
from .validator import ValidationError, HAS_NATIVE_EXT
|
|
11
|
+
|
|
12
|
+
if HAS_NATIVE_EXT:
|
|
13
|
+
try:
|
|
14
|
+
from . import _dhi_native
|
|
15
|
+
except ImportError:
|
|
16
|
+
_dhi_native = None
|
|
17
|
+
else:
|
|
18
|
+
_dhi_native = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BatchValidationResult:
|
|
22
|
+
"""Result of batch validation"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, results: List[bool], valid_count: int, total_count: int):
|
|
25
|
+
self.results = results
|
|
26
|
+
self.valid_count = valid_count
|
|
27
|
+
self.total_count = total_count
|
|
28
|
+
self.invalid_count = total_count - valid_count
|
|
29
|
+
|
|
30
|
+
def is_all_valid(self) -> bool:
|
|
31
|
+
"""Check if all items are valid"""
|
|
32
|
+
return self.valid_count == self.total_count
|
|
33
|
+
|
|
34
|
+
def get_valid_indices(self) -> List[int]:
|
|
35
|
+
"""Get indices of valid items"""
|
|
36
|
+
return [i for i, valid in enumerate(self.results) if valid]
|
|
37
|
+
|
|
38
|
+
def get_invalid_indices(self) -> List[int]:
|
|
39
|
+
"""Get indices of invalid items"""
|
|
40
|
+
return [i for i, valid in enumerate(self.results) if not valid]
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return f"BatchValidationResult(valid={self.valid_count}/{self.total_count})"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def validate_users_batch(
|
|
47
|
+
users: List[Dict[str, Any]],
|
|
48
|
+
name_min: int = 1,
|
|
49
|
+
name_max: int = 100,
|
|
50
|
+
age_min: int = 18,
|
|
51
|
+
age_max: int = 120,
|
|
52
|
+
) -> BatchValidationResult:
|
|
53
|
+
"""
|
|
54
|
+
Validate a batch of users in a single FFI call.
|
|
55
|
+
|
|
56
|
+
This is significantly faster than validating each user individually
|
|
57
|
+
because it makes only ONE call to the native library instead of
|
|
58
|
+
3 calls per user (name, email, age).
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
users: List of user dictionaries with 'name', 'email', 'age' keys
|
|
62
|
+
name_min: Minimum name length (default: 1)
|
|
63
|
+
name_max: Maximum name length (default: 100)
|
|
64
|
+
age_min: Minimum age (default: 18)
|
|
65
|
+
age_max: Maximum age (default: 120)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
BatchValidationResult with validation results for each user
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> users = [
|
|
72
|
+
... {"name": "Alice", "email": "alice@example.com", "age": 25},
|
|
73
|
+
... {"name": "Bob", "email": "bob@example.com", "age": 30},
|
|
74
|
+
... ]
|
|
75
|
+
>>> result = validate_users_batch(users)
|
|
76
|
+
>>> print(f"Valid: {result.valid_count}/{result.total_count}")
|
|
77
|
+
Valid: 2/2
|
|
78
|
+
"""
|
|
79
|
+
if not users:
|
|
80
|
+
return BatchValidationResult([], 0, 0)
|
|
81
|
+
|
|
82
|
+
count = len(users)
|
|
83
|
+
|
|
84
|
+
# Use native extension if available
|
|
85
|
+
if _dhi_native and hasattr(_dhi_native, 'validate_batch_direct'):
|
|
86
|
+
# ULTRA-OPTIMIZED: Pass dicts directly to C with field specs
|
|
87
|
+
# This eliminates ALL Python overhead - C extracts and validates directly!
|
|
88
|
+
field_specs = {
|
|
89
|
+
'name': ('string', name_min, name_max),
|
|
90
|
+
'email': ('email',),
|
|
91
|
+
'age': ('int', age_min, age_max),
|
|
92
|
+
}
|
|
93
|
+
results, valid_count = _dhi_native.validate_batch_direct(users, field_specs)
|
|
94
|
+
return BatchValidationResult(results, valid_count, count)
|
|
95
|
+
|
|
96
|
+
# Fallback: validate individually (slower)
|
|
97
|
+
from .validator import BoundedString, Email, BoundedInt
|
|
98
|
+
|
|
99
|
+
Name = BoundedString(name_min, name_max)
|
|
100
|
+
Age = BoundedInt(age_min, age_max)
|
|
101
|
+
|
|
102
|
+
results = []
|
|
103
|
+
valid_count = 0
|
|
104
|
+
|
|
105
|
+
for user in users:
|
|
106
|
+
try:
|
|
107
|
+
Name.validate(user.get('name', ''))
|
|
108
|
+
Email.validate(user.get('email', ''))
|
|
109
|
+
Age.validate(user.get('age', 0))
|
|
110
|
+
results.append(True)
|
|
111
|
+
valid_count += 1
|
|
112
|
+
except ValidationError:
|
|
113
|
+
results.append(False)
|
|
114
|
+
|
|
115
|
+
return BatchValidationResult(results, valid_count, count)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def validate_ints_batch(
|
|
119
|
+
values: List[int],
|
|
120
|
+
min_val: int,
|
|
121
|
+
max_val: int,
|
|
122
|
+
) -> BatchValidationResult:
|
|
123
|
+
"""
|
|
124
|
+
Validate a batch of integers in a single FFI call.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
values: List of integers to validate
|
|
128
|
+
min_val: Minimum allowed value
|
|
129
|
+
max_val: Maximum allowed value
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
BatchValidationResult with validation results
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
>>> values = [25, 30, 150, 18, 90]
|
|
136
|
+
>>> result = validate_ints_batch(values, 18, 90)
|
|
137
|
+
>>> print(result.get_invalid_indices()) # [2] (150 is out of range)
|
|
138
|
+
[2]
|
|
139
|
+
"""
|
|
140
|
+
if not values:
|
|
141
|
+
return BatchValidationResult([], 0, 0)
|
|
142
|
+
|
|
143
|
+
count = len(values)
|
|
144
|
+
|
|
145
|
+
# Use native extension if available
|
|
146
|
+
if _dhi_native and hasattr(_dhi_native, 'validate_int_batch_simd'):
|
|
147
|
+
results, valid_count = _dhi_native.validate_int_batch_simd(
|
|
148
|
+
values, min_val, max_val
|
|
149
|
+
)
|
|
150
|
+
return BatchValidationResult(results, valid_count, count)
|
|
151
|
+
|
|
152
|
+
# Fallback
|
|
153
|
+
results = [min_val <= v <= max_val for v in values]
|
|
154
|
+
valid_count = sum(results)
|
|
155
|
+
return BatchValidationResult(results, valid_count, count)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def validate_strings_batch(
|
|
159
|
+
strings: List[str],
|
|
160
|
+
min_len: int,
|
|
161
|
+
max_len: int,
|
|
162
|
+
) -> BatchValidationResult:
|
|
163
|
+
"""
|
|
164
|
+
Validate a batch of string lengths in a single FFI call.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
strings: List of strings to validate
|
|
168
|
+
min_len: Minimum allowed length
|
|
169
|
+
max_len: Maximum allowed length
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
BatchValidationResult with validation results
|
|
173
|
+
"""
|
|
174
|
+
if not strings:
|
|
175
|
+
return BatchValidationResult([], 0, 0)
|
|
176
|
+
|
|
177
|
+
count = len(strings)
|
|
178
|
+
|
|
179
|
+
# Use native extension if available
|
|
180
|
+
if _dhi_native and hasattr(_dhi_native, 'validate_string_length_batch'):
|
|
181
|
+
encoded = [s.encode('utf-8') for s in strings]
|
|
182
|
+
results, valid_count = _dhi_native.validate_string_length_batch(
|
|
183
|
+
encoded, min_len, max_len
|
|
184
|
+
)
|
|
185
|
+
return BatchValidationResult(results, valid_count, count)
|
|
186
|
+
|
|
187
|
+
# Fallback
|
|
188
|
+
results = [min_len <= len(s) <= max_len for s in strings]
|
|
189
|
+
valid_count = sum(results)
|
|
190
|
+
return BatchValidationResult(results, valid_count, count)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def validate_emails_batch(emails: List[str]) -> BatchValidationResult:
|
|
194
|
+
"""
|
|
195
|
+
Validate a batch of email addresses in a single FFI call.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
emails: List of email addresses to validate
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
BatchValidationResult with validation results
|
|
202
|
+
"""
|
|
203
|
+
if not emails:
|
|
204
|
+
return BatchValidationResult([], 0, 0)
|
|
205
|
+
|
|
206
|
+
count = len(emails)
|
|
207
|
+
|
|
208
|
+
# Use native extension if available
|
|
209
|
+
if _dhi_native and hasattr(_dhi_native, 'validate_email_batch'):
|
|
210
|
+
encoded = [e.encode('utf-8') for e in emails]
|
|
211
|
+
results, valid_count = _dhi_native.validate_email_batch(encoded)
|
|
212
|
+
return BatchValidationResult(results, valid_count, count)
|
|
213
|
+
|
|
214
|
+
# Fallback
|
|
215
|
+
from .validator import Email
|
|
216
|
+
results = []
|
|
217
|
+
valid_count = 0
|
|
218
|
+
|
|
219
|
+
for email in emails:
|
|
220
|
+
try:
|
|
221
|
+
Email.validate(email)
|
|
222
|
+
results.append(True)
|
|
223
|
+
valid_count += 1
|
|
224
|
+
except ValidationError:
|
|
225
|
+
results.append(False)
|
|
226
|
+
|
|
227
|
+
return BatchValidationResult(results, valid_count, count)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
__all__ = [
|
|
231
|
+
'BatchValidationResult',
|
|
232
|
+
'validate_users_batch',
|
|
233
|
+
'validate_ints_batch',
|
|
234
|
+
'validate_strings_batch',
|
|
235
|
+
'validate_emails_batch',
|
|
236
|
+
]
|
dhi/constraints.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constraint metadata classes for dhi - Pydantic v2 compatible.
|
|
3
|
+
|
|
4
|
+
These classes are used with typing.Annotated to define validation constraints
|
|
5
|
+
on fields, matching the annotated_types / Pydantic v2 pattern.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
from dhi import Gt, Le, MinLength
|
|
10
|
+
|
|
11
|
+
age: Annotated[int, Gt(gt=0), Le(le=120)]
|
|
12
|
+
name: Annotated[str, MinLength(min_length=1)]
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from typing import Optional, Union
|
|
16
|
+
|
|
17
|
+
# Use slots for performance on Python 3.10+, fall back gracefully
|
|
18
|
+
import sys
|
|
19
|
+
_DATACLASS_KWARGS = {"frozen": True}
|
|
20
|
+
if sys.version_info >= (3, 10):
|
|
21
|
+
_DATACLASS_KWARGS["slots"] = True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --- Numeric Constraints ---
|
|
25
|
+
|
|
26
|
+
class Gt:
|
|
27
|
+
"""Greater than constraint."""
|
|
28
|
+
__slots__ = ('gt',)
|
|
29
|
+
|
|
30
|
+
def __init__(self, gt: Union[int, float]):
|
|
31
|
+
object.__setattr__(self, 'gt', gt)
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return f"Gt(gt={self.gt!r})"
|
|
35
|
+
|
|
36
|
+
def __eq__(self, other: object) -> bool:
|
|
37
|
+
return isinstance(other, Gt) and self.gt == other.gt
|
|
38
|
+
|
|
39
|
+
def __hash__(self) -> int:
|
|
40
|
+
return hash(('Gt', self.gt))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Ge:
|
|
44
|
+
"""Greater than or equal constraint."""
|
|
45
|
+
__slots__ = ('ge',)
|
|
46
|
+
|
|
47
|
+
def __init__(self, ge: Union[int, float]):
|
|
48
|
+
object.__setattr__(self, 'ge', ge)
|
|
49
|
+
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
return f"Ge(ge={self.ge!r})"
|
|
52
|
+
|
|
53
|
+
def __eq__(self, other: object) -> bool:
|
|
54
|
+
return isinstance(other, Ge) and self.ge == other.ge
|
|
55
|
+
|
|
56
|
+
def __hash__(self) -> int:
|
|
57
|
+
return hash(('Ge', self.ge))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Lt:
|
|
61
|
+
"""Less than constraint."""
|
|
62
|
+
__slots__ = ('lt',)
|
|
63
|
+
|
|
64
|
+
def __init__(self, lt: Union[int, float]):
|
|
65
|
+
object.__setattr__(self, 'lt', lt)
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
return f"Lt(lt={self.lt!r})"
|
|
69
|
+
|
|
70
|
+
def __eq__(self, other: object) -> bool:
|
|
71
|
+
return isinstance(other, Lt) and self.lt == other.lt
|
|
72
|
+
|
|
73
|
+
def __hash__(self) -> int:
|
|
74
|
+
return hash(('Lt', self.lt))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Le:
|
|
78
|
+
"""Less than or equal constraint."""
|
|
79
|
+
__slots__ = ('le',)
|
|
80
|
+
|
|
81
|
+
def __init__(self, le: Union[int, float]):
|
|
82
|
+
object.__setattr__(self, 'le', le)
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return f"Le(le={self.le!r})"
|
|
86
|
+
|
|
87
|
+
def __eq__(self, other: object) -> bool:
|
|
88
|
+
return isinstance(other, Le) and self.le == other.le
|
|
89
|
+
|
|
90
|
+
def __hash__(self) -> int:
|
|
91
|
+
return hash(('Le', self.le))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MultipleOf:
|
|
95
|
+
"""Multiple of constraint."""
|
|
96
|
+
__slots__ = ('multiple_of',)
|
|
97
|
+
|
|
98
|
+
def __init__(self, multiple_of: Union[int, float]):
|
|
99
|
+
object.__setattr__(self, 'multiple_of', multiple_of)
|
|
100
|
+
|
|
101
|
+
def __repr__(self) -> str:
|
|
102
|
+
return f"MultipleOf(multiple_of={self.multiple_of!r})"
|
|
103
|
+
|
|
104
|
+
def __eq__(self, other: object) -> bool:
|
|
105
|
+
return isinstance(other, MultipleOf) and self.multiple_of == other.multiple_of
|
|
106
|
+
|
|
107
|
+
def __hash__(self) -> int:
|
|
108
|
+
return hash(('MultipleOf', self.multiple_of))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# --- String Constraints ---
|
|
112
|
+
|
|
113
|
+
class MinLength:
|
|
114
|
+
"""Minimum length constraint (for strings, bytes, collections)."""
|
|
115
|
+
__slots__ = ('min_length',)
|
|
116
|
+
|
|
117
|
+
def __init__(self, min_length: int):
|
|
118
|
+
object.__setattr__(self, 'min_length', min_length)
|
|
119
|
+
|
|
120
|
+
def __repr__(self) -> str:
|
|
121
|
+
return f"MinLength(min_length={self.min_length!r})"
|
|
122
|
+
|
|
123
|
+
def __eq__(self, other: object) -> bool:
|
|
124
|
+
return isinstance(other, MinLength) and self.min_length == other.min_length
|
|
125
|
+
|
|
126
|
+
def __hash__(self) -> int:
|
|
127
|
+
return hash(('MinLength', self.min_length))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class MaxLength:
|
|
131
|
+
"""Maximum length constraint (for strings, bytes, collections)."""
|
|
132
|
+
__slots__ = ('max_length',)
|
|
133
|
+
|
|
134
|
+
def __init__(self, max_length: int):
|
|
135
|
+
object.__setattr__(self, 'max_length', max_length)
|
|
136
|
+
|
|
137
|
+
def __repr__(self) -> str:
|
|
138
|
+
return f"MaxLength(max_length={self.max_length!r})"
|
|
139
|
+
|
|
140
|
+
def __eq__(self, other: object) -> bool:
|
|
141
|
+
return isinstance(other, MaxLength) and self.max_length == other.max_length
|
|
142
|
+
|
|
143
|
+
def __hash__(self) -> int:
|
|
144
|
+
return hash(('MaxLength', self.max_length))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Pattern:
|
|
148
|
+
"""Regex pattern constraint for strings."""
|
|
149
|
+
__slots__ = ('pattern',)
|
|
150
|
+
|
|
151
|
+
def __init__(self, pattern: str):
|
|
152
|
+
object.__setattr__(self, 'pattern', pattern)
|
|
153
|
+
|
|
154
|
+
def __repr__(self) -> str:
|
|
155
|
+
return f"Pattern(pattern={self.pattern!r})"
|
|
156
|
+
|
|
157
|
+
def __eq__(self, other: object) -> bool:
|
|
158
|
+
return isinstance(other, Pattern) and self.pattern == other.pattern
|
|
159
|
+
|
|
160
|
+
def __hash__(self) -> int:
|
|
161
|
+
return hash(('Pattern', self.pattern))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Strict:
|
|
165
|
+
"""Strict type checking - no coercion allowed."""
|
|
166
|
+
__slots__ = ('strict',)
|
|
167
|
+
|
|
168
|
+
def __init__(self, strict: bool = True):
|
|
169
|
+
object.__setattr__(self, 'strict', strict)
|
|
170
|
+
|
|
171
|
+
def __repr__(self) -> str:
|
|
172
|
+
return f"Strict(strict={self.strict!r})"
|
|
173
|
+
|
|
174
|
+
def __eq__(self, other: object) -> bool:
|
|
175
|
+
return isinstance(other, Strict) and self.strict == other.strict
|
|
176
|
+
|
|
177
|
+
def __hash__(self) -> int:
|
|
178
|
+
return hash(('Strict', self.strict))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class StripWhitespace:
|
|
182
|
+
"""Strip leading/trailing whitespace from strings."""
|
|
183
|
+
__slots__ = ('strip_whitespace',)
|
|
184
|
+
|
|
185
|
+
def __init__(self, strip_whitespace: bool = True):
|
|
186
|
+
object.__setattr__(self, 'strip_whitespace', strip_whitespace)
|
|
187
|
+
|
|
188
|
+
def __repr__(self) -> str:
|
|
189
|
+
return f"StripWhitespace(strip_whitespace={self.strip_whitespace!r})"
|
|
190
|
+
|
|
191
|
+
def __eq__(self, other: object) -> bool:
|
|
192
|
+
return isinstance(other, StripWhitespace) and self.strip_whitespace == other.strip_whitespace
|
|
193
|
+
|
|
194
|
+
def __hash__(self) -> int:
|
|
195
|
+
return hash(('StripWhitespace', self.strip_whitespace))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ToLower:
|
|
199
|
+
"""Convert string to lowercase."""
|
|
200
|
+
__slots__ = ('to_lower',)
|
|
201
|
+
|
|
202
|
+
def __init__(self, to_lower: bool = True):
|
|
203
|
+
object.__setattr__(self, 'to_lower', to_lower)
|
|
204
|
+
|
|
205
|
+
def __repr__(self) -> str:
|
|
206
|
+
return f"ToLower(to_lower={self.to_lower!r})"
|
|
207
|
+
|
|
208
|
+
def __eq__(self, other: object) -> bool:
|
|
209
|
+
return isinstance(other, ToLower) and self.to_lower == other.to_lower
|
|
210
|
+
|
|
211
|
+
def __hash__(self) -> int:
|
|
212
|
+
return hash(('ToLower', self.to_lower))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class ToUpper:
|
|
216
|
+
"""Convert string to uppercase."""
|
|
217
|
+
__slots__ = ('to_upper',)
|
|
218
|
+
|
|
219
|
+
def __init__(self, to_upper: bool = True):
|
|
220
|
+
object.__setattr__(self, 'to_upper', to_upper)
|
|
221
|
+
|
|
222
|
+
def __repr__(self) -> str:
|
|
223
|
+
return f"ToUpper(to_upper={self.to_upper!r})"
|
|
224
|
+
|
|
225
|
+
def __eq__(self, other: object) -> bool:
|
|
226
|
+
return isinstance(other, ToUpper) and self.to_upper == other.to_upper
|
|
227
|
+
|
|
228
|
+
def __hash__(self) -> int:
|
|
229
|
+
return hash(('ToUpper', self.to_upper))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class AllowInfNan:
|
|
233
|
+
"""Control whether inf/nan float values are allowed."""
|
|
234
|
+
__slots__ = ('allow_inf_nan',)
|
|
235
|
+
|
|
236
|
+
def __init__(self, allow_inf_nan: bool = True):
|
|
237
|
+
object.__setattr__(self, 'allow_inf_nan', allow_inf_nan)
|
|
238
|
+
|
|
239
|
+
def __repr__(self) -> str:
|
|
240
|
+
return f"AllowInfNan(allow_inf_nan={self.allow_inf_nan!r})"
|
|
241
|
+
|
|
242
|
+
def __eq__(self, other: object) -> bool:
|
|
243
|
+
return isinstance(other, AllowInfNan) and self.allow_inf_nan == other.allow_inf_nan
|
|
244
|
+
|
|
245
|
+
def __hash__(self) -> int:
|
|
246
|
+
return hash(('AllowInfNan', self.allow_inf_nan))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class MaxDigits:
|
|
250
|
+
"""Maximum total digits for Decimal types."""
|
|
251
|
+
__slots__ = ('max_digits',)
|
|
252
|
+
|
|
253
|
+
def __init__(self, max_digits: int):
|
|
254
|
+
object.__setattr__(self, 'max_digits', max_digits)
|
|
255
|
+
|
|
256
|
+
def __repr__(self) -> str:
|
|
257
|
+
return f"MaxDigits(max_digits={self.max_digits!r})"
|
|
258
|
+
|
|
259
|
+
def __eq__(self, other: object) -> bool:
|
|
260
|
+
return isinstance(other, MaxDigits) and self.max_digits == other.max_digits
|
|
261
|
+
|
|
262
|
+
def __hash__(self) -> int:
|
|
263
|
+
return hash(('MaxDigits', self.max_digits))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class DecimalPlaces:
|
|
267
|
+
"""Maximum decimal places for Decimal types."""
|
|
268
|
+
__slots__ = ('decimal_places',)
|
|
269
|
+
|
|
270
|
+
def __init__(self, decimal_places: int):
|
|
271
|
+
object.__setattr__(self, 'decimal_places', decimal_places)
|
|
272
|
+
|
|
273
|
+
def __repr__(self) -> str:
|
|
274
|
+
return f"DecimalPlaces(decimal_places={self.decimal_places!r})"
|
|
275
|
+
|
|
276
|
+
def __eq__(self, other: object) -> bool:
|
|
277
|
+
return isinstance(other, DecimalPlaces) and self.decimal_places == other.decimal_places
|
|
278
|
+
|
|
279
|
+
def __hash__(self) -> int:
|
|
280
|
+
return hash(('DecimalPlaces', self.decimal_places))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class UniqueItems:
|
|
284
|
+
"""Ensure collection items are unique."""
|
|
285
|
+
__slots__ = ('unique_items',)
|
|
286
|
+
|
|
287
|
+
def __init__(self, unique_items: bool = True):
|
|
288
|
+
object.__setattr__(self, 'unique_items', unique_items)
|
|
289
|
+
|
|
290
|
+
def __repr__(self) -> str:
|
|
291
|
+
return f"UniqueItems(unique_items={self.unique_items!r})"
|
|
292
|
+
|
|
293
|
+
def __eq__(self, other: object) -> bool:
|
|
294
|
+
return isinstance(other, UniqueItems) and self.unique_items == other.unique_items
|
|
295
|
+
|
|
296
|
+
def __hash__(self) -> int:
|
|
297
|
+
return hash(('UniqueItems', self.unique_items))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# --- Compound Constraint Classes (Pydantic v2 style) ---
|
|
301
|
+
|
|
302
|
+
class StringConstraints:
|
|
303
|
+
"""Combined string constraints - matches Pydantic v2's StringConstraints.
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
from typing import Annotated
|
|
307
|
+
from dhi import StringConstraints
|
|
308
|
+
|
|
309
|
+
Username = Annotated[str, StringConstraints(min_length=3, max_length=20, to_lower=True)]
|
|
310
|
+
"""
|
|
311
|
+
__slots__ = ('min_length', 'max_length', 'pattern', 'strip_whitespace',
|
|
312
|
+
'to_lower', 'to_upper', 'strict')
|
|
313
|
+
|
|
314
|
+
def __init__(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
min_length: Optional[int] = None,
|
|
318
|
+
max_length: Optional[int] = None,
|
|
319
|
+
pattern: Optional[str] = None,
|
|
320
|
+
strip_whitespace: bool = False,
|
|
321
|
+
to_lower: bool = False,
|
|
322
|
+
to_upper: bool = False,
|
|
323
|
+
strict: bool = False,
|
|
324
|
+
):
|
|
325
|
+
object.__setattr__(self, 'min_length', min_length)
|
|
326
|
+
object.__setattr__(self, 'max_length', max_length)
|
|
327
|
+
object.__setattr__(self, 'pattern', pattern)
|
|
328
|
+
object.__setattr__(self, 'strip_whitespace', strip_whitespace)
|
|
329
|
+
object.__setattr__(self, 'to_lower', to_lower)
|
|
330
|
+
object.__setattr__(self, 'to_upper', to_upper)
|
|
331
|
+
object.__setattr__(self, 'strict', strict)
|
|
332
|
+
|
|
333
|
+
def __repr__(self) -> str:
|
|
334
|
+
parts = []
|
|
335
|
+
if self.min_length is not None:
|
|
336
|
+
parts.append(f"min_length={self.min_length}")
|
|
337
|
+
if self.max_length is not None:
|
|
338
|
+
parts.append(f"max_length={self.max_length}")
|
|
339
|
+
if self.pattern is not None:
|
|
340
|
+
parts.append(f"pattern={self.pattern!r}")
|
|
341
|
+
if self.strip_whitespace:
|
|
342
|
+
parts.append("strip_whitespace=True")
|
|
343
|
+
if self.to_lower:
|
|
344
|
+
parts.append("to_lower=True")
|
|
345
|
+
if self.to_upper:
|
|
346
|
+
parts.append("to_upper=True")
|
|
347
|
+
if self.strict:
|
|
348
|
+
parts.append("strict=True")
|
|
349
|
+
return f"StringConstraints({', '.join(parts)})"
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
__all__ = [
|
|
353
|
+
"Gt", "Ge", "Lt", "Le", "MultipleOf",
|
|
354
|
+
"MinLength", "MaxLength", "Pattern",
|
|
355
|
+
"Strict", "StripWhitespace", "ToLower", "ToUpper",
|
|
356
|
+
"AllowInfNan", "MaxDigits", "DecimalPlaces", "UniqueItems",
|
|
357
|
+
"StringConstraints",
|
|
358
|
+
]
|
dhi/datetime_types.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Date/time types for dhi - Pydantic v2 compatible.
|
|
3
|
+
|
|
4
|
+
Provides constrained date and datetime types matching Pydantic's
|
|
5
|
+
date/time type system.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from dhi import BaseModel, PastDate, FutureDatetime, AwareDatetime
|
|
9
|
+
|
|
10
|
+
class Event(BaseModel):
|
|
11
|
+
created: PastDate
|
|
12
|
+
scheduled: FutureDatetime
|
|
13
|
+
meeting_time: AwareDatetime
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from datetime import date, datetime, timezone
|
|
17
|
+
from typing import Annotated, Any
|
|
18
|
+
|
|
19
|
+
from .validator import ValidationError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ============================================================
|
|
23
|
+
# Internal Validator Classes
|
|
24
|
+
# ============================================================
|
|
25
|
+
|
|
26
|
+
class _PastValidator:
|
|
27
|
+
"""Validates that a date/datetime is in the past."""
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str:
|
|
30
|
+
return "PastValidator()"
|
|
31
|
+
|
|
32
|
+
def validate(self, value: Any, field_name: str = "value") -> Any:
|
|
33
|
+
if isinstance(value, datetime):
|
|
34
|
+
now = datetime.now(tz=value.tzinfo)
|
|
35
|
+
if value >= now:
|
|
36
|
+
raise ValidationError(field_name, f"Datetime must be in the past, got {value}")
|
|
37
|
+
elif isinstance(value, date):
|
|
38
|
+
if value >= date.today():
|
|
39
|
+
raise ValidationError(field_name, f"Date must be in the past, got {value}")
|
|
40
|
+
else:
|
|
41
|
+
raise ValidationError(field_name, f"Expected date or datetime, got {type(value).__name__}")
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _FutureValidator:
|
|
46
|
+
"""Validates that a date/datetime is in the future."""
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return "FutureValidator()"
|
|
50
|
+
|
|
51
|
+
def validate(self, value: Any, field_name: str = "value") -> Any:
|
|
52
|
+
if isinstance(value, datetime):
|
|
53
|
+
now = datetime.now(tz=value.tzinfo)
|
|
54
|
+
if value <= now:
|
|
55
|
+
raise ValidationError(field_name, f"Datetime must be in the future, got {value}")
|
|
56
|
+
elif isinstance(value, date):
|
|
57
|
+
if value <= date.today():
|
|
58
|
+
raise ValidationError(field_name, f"Date must be in the future, got {value}")
|
|
59
|
+
else:
|
|
60
|
+
raise ValidationError(field_name, f"Expected date or datetime, got {type(value).__name__}")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _AwareValidator:
|
|
65
|
+
"""Validates that a datetime is timezone-aware."""
|
|
66
|
+
|
|
67
|
+
def __repr__(self) -> str:
|
|
68
|
+
return "AwareValidator()"
|
|
69
|
+
|
|
70
|
+
def validate(self, value: Any, field_name: str = "value") -> datetime:
|
|
71
|
+
if not isinstance(value, datetime):
|
|
72
|
+
raise ValidationError(field_name, f"Expected datetime, got {type(value).__name__}")
|
|
73
|
+
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
|
|
74
|
+
raise ValidationError(field_name, "Datetime must be timezone-aware")
|
|
75
|
+
return value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class _NaiveValidator:
|
|
79
|
+
"""Validates that a datetime is timezone-naive."""
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
return "NaiveValidator()"
|
|
83
|
+
|
|
84
|
+
def validate(self, value: Any, field_name: str = "value") -> datetime:
|
|
85
|
+
if not isinstance(value, datetime):
|
|
86
|
+
raise ValidationError(field_name, f"Expected datetime, got {type(value).__name__}")
|
|
87
|
+
if value.tzinfo is not None and value.tzinfo.utcoffset(value) is not None:
|
|
88
|
+
raise ValidationError(field_name, "Datetime must be timezone-naive")
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ============================================================
|
|
93
|
+
# Public Type Aliases
|
|
94
|
+
# ============================================================
|
|
95
|
+
|
|
96
|
+
PastDate = Annotated[date, _PastValidator()]
|
|
97
|
+
FutureDate = Annotated[date, _FutureValidator()]
|
|
98
|
+
PastDatetime = Annotated[datetime, _PastValidator()]
|
|
99
|
+
FutureDatetime = Annotated[datetime, _FutureValidator()]
|
|
100
|
+
AwareDatetime = Annotated[datetime, _AwareValidator()]
|
|
101
|
+
NaiveDatetime = Annotated[datetime, _NaiveValidator()]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"PastDate", "FutureDate",
|
|
106
|
+
"PastDatetime", "FutureDatetime",
|
|
107
|
+
"AwareDatetime", "NaiveDatetime",
|
|
108
|
+
]
|