hypern 0.3.2__cp312-cp312-win32.whl → 0.3.4__cp312-cp312-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/application.py +71 -13
- hypern/args_parser.py +27 -1
- hypern/config.py +97 -0
- hypern/db/addons/__init__.py +5 -0
- hypern/db/addons/sqlalchemy/__init__.py +71 -0
- hypern/db/sql/__init__.py +13 -179
- hypern/db/sql/field.py +607 -0
- hypern/db/sql/model.py +116 -0
- hypern/db/sql/query.py +904 -0
- hypern/exceptions.py +10 -0
- hypern/hypern.cp312-win32.pyd +0 -0
- hypern/hypern.pyi +32 -0
- hypern/processpool.py +7 -62
- hypern/routing/dispatcher.py +4 -0
- {hypern-0.3.2.dist-info → hypern-0.3.4.dist-info}/METADATA +3 -1
- {hypern-0.3.2.dist-info → hypern-0.3.4.dist-info}/RECORD +27 -22
- /hypern/db/{sql/addons → addons/sqlalchemy/fields}/__init__.py +0 -0
- /hypern/db/{sql/addons → addons/sqlalchemy/fields}/color.py +0 -0
- /hypern/db/{sql/addons → addons/sqlalchemy/fields}/daterange.py +0 -0
- /hypern/db/{sql/addons → addons/sqlalchemy/fields}/datetime.py +0 -0
- /hypern/db/{sql/addons → addons/sqlalchemy/fields}/encrypted.py +0 -0
- /hypern/db/{sql/addons → addons/sqlalchemy/fields}/password.py +0 -0
- /hypern/db/{sql/addons → addons/sqlalchemy/fields}/ts_vector.py +0 -0
- /hypern/db/{sql/addons → addons/sqlalchemy/fields}/unicode.py +0 -0
- /hypern/db/{sql → addons/sqlalchemy}/repository.py +0 -0
- {hypern-0.3.2.dist-info → hypern-0.3.4.dist-info}/WHEEL +0 -0
- {hypern-0.3.2.dist-info → hypern-0.3.4.dist-info}/licenses/LICENSE +0 -0
hypern/db/sql/field.py
ADDED
@@ -0,0 +1,607 @@
|
|
1
|
+
import json
|
2
|
+
import re
|
3
|
+
from datetime import date, datetime, timezone
|
4
|
+
from decimal import Decimal, InvalidOperation
|
5
|
+
from typing import Any, Callable, List, Optional, Union
|
6
|
+
|
7
|
+
from hypern.exceptions import DBFieldValidationError
|
8
|
+
|
9
|
+
|
10
|
+
class Field:
|
11
|
+
"""Base field class for ORM-like field definitions."""
|
12
|
+
|
13
|
+
def __init__(
|
14
|
+
self,
|
15
|
+
field_type: str,
|
16
|
+
primary_key: bool = False,
|
17
|
+
null: bool = True,
|
18
|
+
default: Any = None,
|
19
|
+
unique: bool = False,
|
20
|
+
index: bool = False,
|
21
|
+
validators: Optional[list[Callable]] = None,
|
22
|
+
auto_increment: bool = False,
|
23
|
+
):
|
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
|
+
self.field_type = field_type
|
36
|
+
self.primary_key = primary_key
|
37
|
+
self.null = null
|
38
|
+
self.default = default
|
39
|
+
self.unique = unique
|
40
|
+
self.index = index
|
41
|
+
self.validators = validators or []
|
42
|
+
self.name = None
|
43
|
+
self.model = None
|
44
|
+
self.auto_increment = auto_increment
|
45
|
+
|
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
|
+
"""
|
53
|
+
if value is None:
|
54
|
+
return None
|
55
|
+
return value
|
56
|
+
|
57
|
+
def to_sql_type(self) -> str:
|
58
|
+
"""
|
59
|
+
Get the SQL type representation of the field.
|
60
|
+
|
61
|
+
:return: SQL type string
|
62
|
+
"""
|
63
|
+
type_mapping = {
|
64
|
+
"int": "INTEGER",
|
65
|
+
"str": "VARCHAR(255)",
|
66
|
+
"float": "FLOAT",
|
67
|
+
"bool": "BOOLEAN",
|
68
|
+
"datetime": "TIMESTAMP",
|
69
|
+
"date": "DATE",
|
70
|
+
"text": "TEXT",
|
71
|
+
"json": "JSONB",
|
72
|
+
"array": "ARRAY",
|
73
|
+
"decimal": "DECIMAL",
|
74
|
+
}
|
75
|
+
return type_mapping.get(self.field_type, "VARCHAR(255)")
|
76
|
+
|
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
|
+
|
98
|
+
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)
|
110
|
+
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
|
+
|
124
|
+
def validate(self, value: Any) -> None:
|
125
|
+
"""
|
126
|
+
Validate character field constraints.
|
127
|
+
|
128
|
+
:param value: Value to validate
|
129
|
+
"""
|
130
|
+
super().validate(value)
|
131
|
+
|
132
|
+
if value is None:
|
133
|
+
return
|
134
|
+
|
135
|
+
# Convert to string for validation
|
136
|
+
str_value = str(value)
|
137
|
+
|
138
|
+
# Length validation
|
139
|
+
if len(str_value) > self.max_length:
|
140
|
+
raise DBFieldValidationError(f"Value exceeds max length of {self.max_length}")
|
141
|
+
|
142
|
+
if len(str_value) < self.min_length:
|
143
|
+
raise DBFieldValidationError(f"Value is shorter than min length of {self.min_length}")
|
144
|
+
|
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}")
|
148
|
+
|
149
|
+
|
150
|
+
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")
|
172
|
+
|
173
|
+
def validate(self, value: Any) -> None:
|
174
|
+
"""
|
175
|
+
Validate integer field constraints.
|
176
|
+
|
177
|
+
:param value: Value to validate
|
178
|
+
"""
|
179
|
+
super().validate(value)
|
180
|
+
|
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
|
+
|
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
|
230
|
+
|
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")
|
233
|
+
|
234
|
+
return decimal_value
|
235
|
+
except (TypeError, ValueError, InvalidOperation):
|
236
|
+
raise DBFieldValidationError(f"Cannot convert {value} to Decimal")
|
237
|
+
|
238
|
+
def to_sql_type(self) -> str:
|
239
|
+
"""Get SQL type with defined precision."""
|
240
|
+
return f"DECIMAL({self.max_digits},{self.decimal_places})"
|
241
|
+
|
242
|
+
def validate(self, value: Any) -> None:
|
243
|
+
"""
|
244
|
+
Validate decimal field constraints.
|
245
|
+
|
246
|
+
:param value: Value to validate
|
247
|
+
"""
|
248
|
+
super().validate(value)
|
249
|
+
|
250
|
+
if value is None:
|
251
|
+
return
|
252
|
+
|
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}")
|
258
|
+
|
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}")
|
261
|
+
|
262
|
+
|
263
|
+
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)
|
276
|
+
self.auto_now = auto_now
|
277
|
+
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
|
+
|
294
|
+
def validate(self, value: Any) -> None:
|
295
|
+
"""
|
296
|
+
Validate date field constraints.
|
297
|
+
|
298
|
+
:param value: Value to validate
|
299
|
+
"""
|
300
|
+
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}")
|
313
|
+
|
314
|
+
|
315
|
+
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")
|
342
|
+
|
343
|
+
def validate(self, value: Any) -> None:
|
344
|
+
"""
|
345
|
+
Validate JSON field constraints.
|
346
|
+
|
347
|
+
:param value: Value to validate
|
348
|
+
"""
|
349
|
+
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
|
+
|
361
|
+
try:
|
362
|
+
validate(instance=json_value, schema=self.schema)
|
363
|
+
except JsonSchemaError as e:
|
364
|
+
raise DBFieldValidationError(f"JSON schema validation failed: {str(e)}")
|
365
|
+
|
366
|
+
|
367
|
+
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)
|
379
|
+
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
|
+
|
411
|
+
def validate(self, value: Any) -> None:
|
412
|
+
"""
|
413
|
+
Validate array field constraints.
|
414
|
+
|
415
|
+
:param value: Value to validate
|
416
|
+
"""
|
417
|
+
super().validate(value)
|
418
|
+
|
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")
|
431
|
+
|
432
|
+
# Validate each element using base field's validate method
|
433
|
+
for item in list_value:
|
434
|
+
self.base_field.validate(item)
|
435
|
+
|
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"
|
482
|
+
|
483
|
+
def validate(self, value: Any) -> None:
|
484
|
+
"""
|
485
|
+
Validate foreign key constraints.
|
486
|
+
|
487
|
+
:param value: Value to validate
|
488
|
+
"""
|
489
|
+
super().validate(value)
|
490
|
+
|
491
|
+
|
492
|
+
class DateTimeField(Field):
|
493
|
+
"""DateTime field with advanced validation and auto-update capabilities."""
|
494
|
+
|
495
|
+
def __init__(
|
496
|
+
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,
|
502
|
+
**kwargs,
|
503
|
+
):
|
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)
|
567
|
+
else:
|
568
|
+
# Remove timezone if not required
|
569
|
+
dt = dt.replace(tzinfo=None)
|
570
|
+
|
571
|
+
return dt
|
572
|
+
|
573
|
+
def to_sql_type(self) -> str:
|
574
|
+
"""
|
575
|
+
Get SQL type for datetime.
|
576
|
+
|
577
|
+
:return: SQL timestamp type string
|
578
|
+
"""
|
579
|
+
return "TIMESTAMP"
|
580
|
+
|
581
|
+
def validate(self, value: Any) -> None:
|
582
|
+
"""
|
583
|
+
Validate datetime field constraints.
|
584
|
+
|
585
|
+
:param value: Value to validate
|
586
|
+
"""
|
587
|
+
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")
|