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.
- hypern/__init__.py +21 -1
- hypern/application.py +26 -36
- hypern/args_parser.py +0 -26
- hypern/database/sql/__init__.py +24 -1
- hypern/database/sql/field.py +130 -491
- hypern/database/sql/migrate.py +263 -0
- hypern/database/sql/model.py +4 -3
- hypern/database/sql/query.py +2 -2
- hypern/datastructures.py +2 -2
- hypern/hypern.cp311-win32.pyd +0 -0
- hypern/hypern.pyi +4 -9
- hypern/openapi/schemas.py +5 -7
- hypern/routing/route.py +8 -12
- hypern/worker.py +265 -21
- {hypern-0.3.6.dist-info → hypern-0.3.8.dist-info}/METADATA +16 -6
- {hypern-0.3.6.dist-info → hypern-0.3.8.dist-info}/RECORD +18 -18
- {hypern-0.3.6.dist-info → hypern-0.3.8.dist-info}/WHEEL +1 -1
- hypern/ws.py +0 -16
- {hypern-0.3.6.dist-info → hypern-0.3.8.dist-info}/licenses/LICENSE +0 -0
hypern/database/sql/field.py
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
import json
|
2
|
-
import
|
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,
|
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
|
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
|
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
|
-
|
55
|
-
|
36
|
+
if not self.null:
|
37
|
+
raise DBFieldValidationError(f"Field {self.name} cannot be null")
|
38
|
+
return
|
56
39
|
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
133
|
-
|
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
|
-
|
143
|
-
|
80
|
+
class TextField(Field):
|
81
|
+
def __init__(self, **kwargs):
|
82
|
+
super().__init__(field_type="text", **kwargs)
|
144
83
|
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
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
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
232
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
260
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
363
|
-
except
|
364
|
-
raise DBFieldValidationError(f"
|
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
|
-
|
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
|
-
|
420
|
-
|
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
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
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
|
493
|
-
"""
|
203
|
+
class ForeignKeyField(Field):
|
204
|
+
"""Field for foreign key relationships."""
|
494
205
|
|
495
206
|
def __init__(
|
496
207
|
self,
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
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
|
-
|
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
|
-
|
569
|
-
|
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
|
-
|
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
|
-
|
574
|
-
|
575
|
-
|
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
|
-
|
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
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
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)}")
|