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/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
+ ]