hypern 0.3.6__cp311-cp311-win32.whl → 0.3.8__cp311-cp311-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,7 @@
1
1
  import json
2
- import re
3
- from datetime import date, datetime, timezone
2
+ from datetime import date, datetime
4
3
  from decimal import Decimal, InvalidOperation
5
- from typing import Any, Callable, List, Optional, Union
4
+ from typing import Any, Optional, Union
6
5
 
7
6
  from hypern.exceptions import DBFieldValidationError
8
7
 
@@ -18,20 +17,9 @@ class Field:
18
17
  default: Any = None,
19
18
  unique: bool = False,
20
19
  index: bool = False,
21
- validators: Optional[list[Callable]] = None,
20
+ validators: Optional[list] = None,
22
21
  auto_increment: bool = False,
23
22
  ):
24
- """
25
- Initialize a field with various constraints and validation options.
26
-
27
- :param field_type: Type of the field
28
- :param primary_key: Whether the field is a primary key
29
- :param null: Whether the field can be null
30
- :param default: Default value for the field
31
- :param unique: Whether the field value must be unique
32
- :param index: Whether to create an index for this field
33
- :param validators: List of custom validator functions
34
- """
35
23
  self.field_type = field_type
36
24
  self.primary_key = primary_key
37
25
  self.null = null
@@ -43,23 +31,20 @@ class Field:
43
31
  self.model = None
44
32
  self.auto_increment = auto_increment
45
33
 
46
- def to_py_type(self, value: Any) -> Any:
47
- """
48
- Convert input value to the field's Python type.
49
-
50
- :param value: Input value to convert
51
- :return: Converted value
52
- """
34
+ def validate(self, value: Any) -> None:
53
35
  if value is None:
54
- return None
55
- return value
36
+ if not self.null:
37
+ raise DBFieldValidationError(f"Field {self.name} cannot be null")
38
+ return
56
39
 
57
- def to_sql_type(self) -> str:
58
- """
59
- Get the SQL type representation of the field.
40
+ for validator in self.validators:
41
+ try:
42
+ validator(value)
43
+ except Exception as e:
44
+ raise DBFieldValidationError(f"Validation failed for {self.name}: {str(e)}")
60
45
 
61
- :return: SQL type string
62
- """
46
+ def sql_type(self) -> str:
47
+ """Return SQL type definition for the field."""
63
48
  type_mapping = {
64
49
  "int": "INTEGER",
65
50
  "str": "VARCHAR(255)",
@@ -74,534 +59,188 @@ class Field:
74
59
  }
75
60
  return type_mapping.get(self.field_type, "VARCHAR(255)")
76
61
 
77
- def validate(self, value: Any) -> None:
78
- """
79
- Validate the input value against field constraints.
80
-
81
- :param value: Value to validate
82
- :raises DBFieldValidationError: If validation fails
83
- """
84
- # Null check
85
- if value is None:
86
- if not self.null:
87
- raise DBFieldValidationError(f"Field {self.name} cannot be null")
88
- return
89
-
90
- # Run custom validators
91
- for validator in self.validators:
92
- try:
93
- validator(value)
94
- except Exception as e:
95
- raise DBFieldValidationError(f"Validation failed for {self.name}: {str(e)}")
96
-
97
62
 
98
63
  class CharField(Field):
99
- """Character field with max length constraint."""
100
-
101
- def __init__(self, max_length: int = 255, min_length: int = 0, regex: Optional[str] = None, **kwargs):
102
- """
103
- Initialize a character field.
104
-
105
- :param max_length: Maximum allowed length
106
- :param min_length: Minimum allowed length
107
- :param regex: Optional regex pattern for validation
108
- """
109
- super().__init__("str", **kwargs)
64
+ def __init__(self, max_length: int = 255, **kwargs):
65
+ super().__init__(field_type="str", **kwargs)
110
66
  self.max_length = max_length
111
- self.min_length = min_length
112
- self.regex = regex
113
-
114
- def to_py_type(self, value: Any) -> Optional[str]:
115
- """Convert input to string."""
116
- if value is None:
117
- return None
118
- return str(value)
119
-
120
- def to_sql_type(self) -> str:
121
- """Get SQL type with defined max length."""
122
- return f"VARCHAR({self.max_length})"
123
67
 
124
68
  def validate(self, value: Any) -> None:
125
- """
126
- Validate character field constraints.
127
-
128
- :param value: Value to validate
129
- """
130
69
  super().validate(value)
70
+ if value is not None:
71
+ if not isinstance(value, str):
72
+ raise DBFieldValidationError(f"Field {self.name} must be a string")
73
+ if len(value) > self.max_length:
74
+ raise DBFieldValidationError(f"Field {self.name} cannot exceed {self.max_length} characters")
131
75
 
132
- if value is None:
133
- return
134
-
135
- # Convert to string for validation
136
- str_value = str(value)
76
+ def sql_type(self) -> str:
77
+ return f"VARCHAR({self.max_length})"
137
78
 
138
- # Length validation
139
- if len(str_value) > self.max_length:
140
- raise DBFieldValidationError(f"Value exceeds max length of {self.max_length}")
141
79
 
142
- if len(str_value) < self.min_length:
143
- raise DBFieldValidationError(f"Value is shorter than min length of {self.min_length}")
80
+ class TextField(Field):
81
+ def __init__(self, **kwargs):
82
+ super().__init__(field_type="text", **kwargs)
144
83
 
145
- # Regex validation
146
- if self.regex and not re.match(self.regex, str_value):
147
- raise DBFieldValidationError(f"Value does not match required pattern: {self.regex}")
84
+ def validate(self, value: Any) -> None:
85
+ super().validate(value)
86
+ if value is not None and not isinstance(value, str):
87
+ raise DBFieldValidationError(f"Field {self.name} must be a string")
148
88
 
149
89
 
150
90
  class IntegerField(Field):
151
- """Integer field with range constraints."""
152
-
153
- def __init__(self, min_value: Optional[int] = None, max_value: Optional[int] = None, **kwargs):
154
- """
155
- Initialize an integer field.
156
-
157
- :param min_value: Minimum allowed value
158
- :param max_value: Maximum allowed value
159
- """
160
- super().__init__("int", **kwargs)
161
- self.min_value = min_value
162
- self.max_value = max_value
163
-
164
- def to_py_type(self, value: Any) -> Optional[int]:
165
- """Convert input to integer."""
166
- if value is None:
167
- return None
168
- try:
169
- return int(value)
170
- except (TypeError, ValueError):
171
- raise DBFieldValidationError(f"Cannot convert {value} to integer")
91
+ def __init__(self, **kwargs):
92
+ super().__init__(field_type="int", **kwargs)
172
93
 
173
94
  def validate(self, value: Any) -> None:
174
- """
175
- Validate integer field constraints.
176
-
177
- :param value: Value to validate
178
- """
179
95
  super().validate(value)
96
+ if value is not None:
97
+ try:
98
+ int(value)
99
+ except (TypeError, ValueError):
100
+ raise DBFieldValidationError(f"Field {self.name} must be an integer")
180
101
 
181
- if value is None:
182
- return
183
-
184
- int_value = self.to_py_type(value)
185
-
186
- # Range validation
187
- if self.min_value is not None and int_value < self.min_value:
188
- raise DBFieldValidationError(f"Value must be >= {self.min_value}")
189
-
190
- if self.max_value is not None and int_value > self.max_value:
191
- raise DBFieldValidationError(f"Value must be <= {self.max_value}")
192
-
193
-
194
- class DecimalField(Field):
195
- """Decimal field with precision and scale constraints."""
196
-
197
- def __init__(
198
- self,
199
- max_digits: int = 10,
200
- decimal_places: int = 2,
201
- min_value: Optional[Union[int, float, Decimal]] = None,
202
- max_value: Optional[Union[int, float, Decimal]] = None,
203
- **kwargs,
204
- ):
205
- """
206
- Initialize a decimal field.
207
-
208
- :param max_digits: Total number of digits
209
- :param decimal_places: Number of decimal places
210
- :param min_value: Minimum allowed value
211
- :param max_value: Maximum allowed value
212
- """
213
- super().__init__("decimal", **kwargs)
214
- self.max_digits = max_digits
215
- self.decimal_places = decimal_places
216
- self.min_value = min_value
217
- self.max_value = max_value
218
-
219
- def to_py_type(self, value: Any) -> Optional[Decimal]:
220
- """Convert input to Decimal."""
221
- if value is None:
222
- return None
223
- try:
224
- decimal_value = Decimal(str(value))
225
102
 
226
- # Check precision
227
- parts = str(decimal_value).split(".")
228
- total_digits = len(parts[0].lstrip("-")) + (len(parts[1]) if len(parts) > 1 else 0)
229
- decimal_digits = len(parts[1]) if len(parts) > 1 else 0
103
+ class FloatField(Field):
104
+ def __init__(self, **kwargs):
105
+ super().__init__(field_type="float", **kwargs)
230
106
 
231
- if total_digits > self.max_digits or decimal_digits > self.decimal_places:
232
- raise DBFieldValidationError(f"Decimal exceeds precision: {self.max_digits} digits, {self.decimal_places} decimal places")
107
+ def validate(self, value: Any) -> None:
108
+ super().validate(value)
109
+ if value is not None:
110
+ try:
111
+ float(value)
112
+ except (TypeError, ValueError):
113
+ raise DBFieldValidationError(f"Field {self.name} must be a float")
233
114
 
234
- return decimal_value
235
- except (TypeError, ValueError, InvalidOperation):
236
- raise DBFieldValidationError(f"Cannot convert {value} to Decimal")
237
115
 
238
- def to_sql_type(self) -> str:
239
- """Get SQL type with defined precision."""
240
- return f"DECIMAL({self.max_digits},{self.decimal_places})"
116
+ class BooleanField(Field):
117
+ def __init__(self, **kwargs):
118
+ super().__init__(field_type="bool", **kwargs)
241
119
 
242
120
  def validate(self, value: Any) -> None:
243
- """
244
- Validate decimal field constraints.
245
-
246
- :param value: Value to validate
247
- """
248
121
  super().validate(value)
122
+ if value is not None and not isinstance(value, bool):
123
+ raise DBFieldValidationError(f"Field {self.name} must be a boolean")
249
124
 
250
- if value is None:
251
- return
252
125
 
253
- decimal_value = self.to_py_type(value)
254
-
255
- # Range validation
256
- if self.min_value is not None and decimal_value < Decimal(str(self.min_value)):
257
- raise DBFieldValidationError(f"Value must be >= {self.min_value}")
126
+ class DateTimeField(Field):
127
+ def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs):
128
+ super().__init__(field_type="datetime", **kwargs)
129
+ self.auto_now = auto_now
130
+ self.auto_now_add = auto_now_add
258
131
 
259
- if self.max_value is not None and decimal_value > Decimal(str(self.max_value)):
260
- raise DBFieldValidationError(f"Value must be <= {self.max_value}")
132
+ def validate(self, value: Any) -> None:
133
+ super().validate(value)
134
+ if value is not None and not isinstance(value, datetime):
135
+ raise DBFieldValidationError(f"Field {self.name} must be a datetime object")
261
136
 
262
137
 
263
138
  class DateField(Field):
264
- """Date field with range constraints."""
265
-
266
- def __init__(self, auto_now: bool = False, auto_now_add: bool = False, min_date: Optional[date] = None, max_date: Optional[date] = None, **kwargs):
267
- """
268
- Initialize a date field.
269
-
270
- :param auto_now: Update to current date on every save
271
- :param auto_now_add: Set to current date when first created
272
- :param min_date: Minimum allowed date
273
- :param max_date: Maximum allowed date
274
- """
275
- super().__init__("date", **kwargs)
139
+ def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs):
140
+ super().__init__(field_type="date", **kwargs)
276
141
  self.auto_now = auto_now
277
142
  self.auto_now_add = auto_now_add
278
- self.min_date = min_date
279
- self.max_date = max_date
280
-
281
- def to_py_type(self, value: Any) -> Optional[date]:
282
- """Convert input to date."""
283
- if value is None:
284
- return None
285
-
286
- if isinstance(value, date):
287
- return value
288
-
289
- try:
290
- return date.fromisoformat(str(value))
291
- except ValueError:
292
- raise DBFieldValidationError(f"Cannot convert {value} to date")
293
143
 
294
144
  def validate(self, value: Any) -> None:
295
- """
296
- Validate date field constraints.
297
-
298
- :param value: Value to validate
299
- """
300
145
  super().validate(value)
301
-
302
- if value is None:
303
- return
304
-
305
- date_value = self.to_py_type(value)
306
-
307
- # Range validation
308
- if self.min_date is not None and date_value < self.min_date:
309
- raise DBFieldValidationError(f"Date must be >= {self.min_date}")
310
-
311
- if self.max_date is not None and date_value > self.max_date:
312
- raise DBFieldValidationError(f"Date must be <= {self.max_date}")
146
+ if value is not None and not isinstance(value, date):
147
+ raise DBFieldValidationError(f"Field {self.name} must be a date object")
313
148
 
314
149
 
315
150
  class JSONField(Field):
316
- """JSON field with optional schema validation."""
317
-
318
- def __init__(self, schema: Optional[dict] = None, **kwargs):
319
- """
320
- Initialize a JSON field.
321
-
322
- :param schema: Optional JSON schema for validation
323
- """
324
- super().__init__("json", **kwargs)
325
- self.schema = schema
326
-
327
- def to_py_type(self, value: Any) -> Optional[dict]:
328
- """Convert input to JSON."""
329
- if value is None:
330
- return None
331
-
332
- if isinstance(value, str):
333
- try:
334
- return json.loads(value)
335
- except json.JSONDecodeError:
336
- raise DBFieldValidationError(f"Invalid JSON string: {value}")
337
-
338
- if isinstance(value, dict):
339
- return value
340
-
341
- raise DBFieldValidationError(f"Cannot convert {value} to JSON")
151
+ def __init__(self, **kwargs):
152
+ super().__init__(field_type="json", **kwargs)
342
153
 
343
154
  def validate(self, value: Any) -> None:
344
- """
345
- Validate JSON field constraints.
346
-
347
- :param value: Value to validate
348
- """
349
155
  super().validate(value)
350
-
351
- if value is None:
352
- return
353
-
354
- json_value = self.to_py_type(value)
355
-
356
- # Schema validation
357
- if self.schema:
358
- from jsonschema import DBFieldValidationError as JsonSchemaError
359
- from jsonschema import validate
360
-
156
+ if value is not None:
361
157
  try:
362
- validate(instance=json_value, schema=self.schema)
363
- except JsonSchemaError as e:
364
- raise DBFieldValidationError(f"JSON schema validation failed: {str(e)}")
158
+ json.dumps(value)
159
+ except (TypeError, ValueError):
160
+ raise DBFieldValidationError(f"Field {self.name} must be JSON serializable")
365
161
 
366
162
 
367
163
  class ArrayField(Field):
368
- """Array field with base field type validation."""
369
-
370
- def __init__(self, base_field: Field, min_length: Optional[int] = None, max_length: Optional[int] = None, **kwargs):
371
- """
372
- Initialize an array field.
373
-
374
- :param base_field: Field type for array elements
375
- :param min_length: Minimum number of elements
376
- :param max_length: Maximum number of elements
377
- """
378
- super().__init__("array", **kwargs)
164
+ def __init__(self, base_field: Field, **kwargs):
165
+ super().__init__(field_type="array", **kwargs)
379
166
  self.base_field = base_field
380
- self.min_length = min_length
381
- self.max_length = max_length
382
-
383
- def to_py_type(self, value: Any) -> Optional[List[Any]]:
384
- """
385
- Convert input to a list with base field type conversion.
386
-
387
- :param value: Input value to convert
388
- :return: Converted list
389
- """
390
- if value is None:
391
- return None
392
-
393
- # Ensure input is a list
394
- if not isinstance(value, list):
395
- try:
396
- value = list(value)
397
- except TypeError:
398
- raise DBFieldValidationError(f"Cannot convert {value} to list")
399
-
400
- # Convert each element using base field's to_py_type
401
- return [self.base_field.to_py_type(item) for item in value]
402
-
403
- def to_sql_type(self) -> str:
404
- """
405
- Get SQL type representation of the array.
406
-
407
- :return: SQL array type string
408
- """
409
- return f"{self.base_field.to_sql_type()}[]"
410
167
 
411
168
  def validate(self, value: Any) -> None:
412
- """
413
- Validate array field constraints.
414
-
415
- :param value: Value to validate
416
- """
417
169
  super().validate(value)
170
+ if value is not None:
171
+ if not isinstance(value, (list, tuple)):
172
+ raise DBFieldValidationError(f"Field {self.name} must be a list or tuple")
173
+ for item in value:
174
+ self.base_field.validate(item)
418
175
 
419
- if value is None:
420
- return
421
-
422
- # Ensure we have a list
423
- list_value = self.to_py_type(value)
424
-
425
- # Length validation
426
- if self.min_length is not None and len(list_value) < self.min_length:
427
- raise DBFieldValidationError(f"Array must have at least {self.min_length} elements")
428
-
429
- if self.max_length is not None and len(list_value) > self.max_length:
430
- raise DBFieldValidationError(f"Array must have no more than {self.max_length} elements")
176
+ def sql_type(self) -> str:
177
+ return f"{self.base_field.sql_type()}[]"
431
178
 
432
- # Validate each element using base field's validate method
433
- for item in list_value:
434
- self.base_field.validate(item)
435
179
 
436
-
437
- class ForeignKey(Field):
438
- """Foreign key field representing a relationship to another model."""
439
-
440
- def __init__(self, to_model: str, related_field: str, on_delete: str = "CASCADE", on_update: str = "CASCADE", **kwargs):
441
- """
442
- Initialize a foreign key field.
443
-
444
- :param to_model: Name of the related model
445
- :param on_delete: Action to take on related record deletion
446
- :param on_update: Action to take on related record update
447
- """
448
- # Allow overriding primary key and null status if not specified
449
- if "primary_key" not in kwargs:
450
- kwargs["primary_key"] = False
451
- if "null" not in kwargs:
452
- kwargs["null"] = False
453
-
454
- super().__init__("int", **kwargs)
455
- self.to_model = to_model
456
- self.on_delete = on_delete
457
- self.on_update = on_update
458
- self.related_field = related_field
459
-
460
- def to_py_type(self, value: Any) -> Optional[int]:
461
- """
462
- Convert input to integer representing foreign key.
463
-
464
- :param value: Value to convert
465
- :return: Converted integer
466
- """
467
- if value is None:
468
- return None
469
-
470
- try:
471
- return int(value)
472
- except (TypeError, ValueError):
473
- raise DBFieldValidationError(f"Cannot convert {value} to integer foreign key")
474
-
475
- def to_sql_type(self) -> str:
476
- """
477
- Get SQL type for foreign key.
478
-
479
- :return: SQL integer type string
480
- """
481
- return "INTEGER"
180
+ class DecimalField(Field):
181
+ def __init__(self, max_digits: int = 10, decimal_places: int = 2, **kwargs):
182
+ super().__init__(field_type="decimal", **kwargs)
183
+ self.max_digits = max_digits
184
+ self.decimal_places = decimal_places
482
185
 
483
186
  def validate(self, value: Any) -> None:
484
- """
485
- Validate foreign key constraints.
486
-
487
- :param value: Value to validate
488
- """
489
187
  super().validate(value)
188
+ if value is not None:
189
+ try:
190
+ decimal_value = Decimal(str(value))
191
+ decimal_tuple = decimal_value.as_tuple()
192
+ if len(decimal_tuple.digits) - (-decimal_tuple.exponent) > self.max_digits:
193
+ raise DBFieldValidationError(f"Field {self.name} exceeds maximum digits {self.max_digits}")
194
+ if -decimal_tuple.exponent > self.decimal_places:
195
+ raise DBFieldValidationError(f"Field {self.name} exceeds maximum decimal places {self.decimal_places}")
196
+ except InvalidOperation:
197
+ raise DBFieldValidationError(f"Field {self.name} must be a valid decimal number")
198
+
199
+ def sql_type(self) -> str:
200
+ return f"DECIMAL({self.max_digits},{self.decimal_places})"
490
201
 
491
202
 
492
- class DateTimeField(Field):
493
- """DateTime field with advanced validation and auto-update capabilities."""
203
+ class ForeignKeyField(Field):
204
+ """Field for foreign key relationships."""
494
205
 
495
206
  def __init__(
496
207
  self,
497
- auto_now: bool = False,
498
- auto_now_add: bool = False,
499
- min_datetime: Optional[datetime] = None,
500
- max_datetime: Optional[datetime] = None,
501
- timezone_aware: bool = True,
208
+ to_model: Union[str, Any],
209
+ related_field: str = "id",
210
+ on_delete: str = "CASCADE",
211
+ on_update: str = "CASCADE",
212
+ related_name: Optional[str] = None,
502
213
  **kwargs,
503
214
  ):
504
- """
505
- Initialize a datetime field.
506
-
507
- :param auto_now: Update to current datetime on every save
508
- :param auto_now_add: Set to current datetime when first created
509
- :param min_datetime: Minimum allowed datetime
510
- :param max_datetime: Maximum allowed datetime
511
- :param timezone_aware: Enforce timezone awareness
512
- """
513
- super().__init__("datetime", **kwargs)
514
- self.auto_now = auto_now
515
- self.auto_now_add = auto_now_add
516
- self.min_datetime = min_datetime
517
- self.max_datetime = max_datetime
518
- self.timezone_aware = timezone_aware
519
-
520
- def to_py_type(self, value: Any) -> Optional[datetime]:
521
- """
522
- Convert input to datetime with robust parsing.
523
-
524
- :param value: Value to convert
525
- :return: Converted datetime
526
- """
527
- if value is None:
528
- return None
529
-
530
- # If already a datetime, handle timezone
531
- if isinstance(value, datetime):
532
- return self._handle_timezone(value)
533
-
534
- # String parsing with multiple formats
535
- if isinstance(value, str):
536
- try:
537
- # ISO format parsing
538
- parsed_datetime = datetime.fromisoformat(value)
539
- return self._handle_timezone(parsed_datetime)
540
- except ValueError:
541
- # Additional parsing formats can be added
542
- try:
543
- # Alternative parsing (e.g., common formats)
544
- parsed_datetime = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
545
- return self._handle_timezone(parsed_datetime)
546
- except ValueError:
547
- raise DBFieldValidationError(f"Cannot parse datetime from: {value}")
548
-
549
- # Attempt generic conversion
550
- try:
551
- converted_datetime = datetime.fromtimestamp(float(value))
552
- return self._handle_timezone(converted_datetime)
553
- except (TypeError, ValueError):
554
- raise DBFieldValidationError(f"Cannot convert {value} to datetime")
555
-
556
- def _handle_timezone(self, dt: datetime) -> datetime:
557
- """
558
- Handle timezone requirements.
559
-
560
- :param dt: Input datetime
561
- :return: Timezone-adjusted datetime
562
- """
563
- if self.timezone_aware:
564
- # If no timezone, assume UTC
565
- if dt.tzinfo is None:
566
- dt = dt.replace(tzinfo=timezone.utc)
215
+ if isinstance(to_model, str):
216
+ field_type = "int"
567
217
  else:
568
- # Remove timezone if not required
569
- dt = dt.replace(tzinfo=None)
218
+ related_field_obj = getattr(to_model, related_field, None)
219
+ if related_field_obj is None:
220
+ raise ValueError(f"Field {related_field} not found in model {to_model.__name__}")
221
+ field_type = related_field_obj.field_type
570
222
 
571
- return dt
223
+ super().__init__(field_type=field_type, **kwargs)
224
+ self.to_model = to_model
225
+ self.related_field = related_field
226
+ self.on_delete = on_delete.upper()
227
+ self.on_update = on_update.upper()
228
+ self.related_name = related_name
572
229
 
573
- def to_sql_type(self) -> str:
574
- """
575
- Get SQL type for datetime.
230
+ valid_actions = {"CASCADE", "SET NULL", "RESTRICT", "NO ACTION"}
231
+ if self.on_delete not in valid_actions:
232
+ raise ValueError(f"Invalid on_delete action. Must be one of: {valid_actions}")
233
+ if self.on_update not in valid_actions:
234
+ raise ValueError(f"Invalid on_update action. Must be one of: {valid_actions}")
576
235
 
577
- :return: SQL timestamp type string
578
- """
579
- return "TIMESTAMP"
236
+ if (self.on_delete == "SET NULL" or self.on_update == "SET NULL") and not kwargs.get("null", True):
237
+ raise ValueError("Field must be nullable to use SET NULL referential action")
580
238
 
581
239
  def validate(self, value: Any) -> None:
582
- """
583
- Validate datetime field constraints.
584
-
585
- :param value: Value to validate
586
- """
587
240
  super().validate(value)
588
-
589
- if value is None:
590
- return
591
-
592
- datetime_value = self.to_py_type(value)
593
-
594
- # Range validation
595
- if self.min_datetime is not None:
596
- min_dt = self._handle_timezone(self.min_datetime)
597
- if datetime_value < min_dt:
598
- raise DBFieldValidationError(f"Datetime must be >= {min_dt}")
599
-
600
- if self.max_datetime is not None:
601
- max_dt = self._handle_timezone(self.max_datetime)
602
- if datetime_value > max_dt:
603
- raise DBFieldValidationError(f"Datetime must be <= {max_dt}")
604
-
605
- # Timezone awareness check
606
- if self.timezone_aware and datetime_value.tzinfo is None:
607
- raise DBFieldValidationError("Datetime must be timezone-aware")
241
+ if value is not None and not isinstance(self.to_model, str):
242
+ related_field_obj = getattr(self.to_model, self.related_field)
243
+ try:
244
+ related_field_obj.validate(value)
245
+ except DBFieldValidationError as e:
246
+ raise DBFieldValidationError(f"Foreign key {self.name} validation failed: {str(e)}")