everysk-lib 1.10.3__cp312-cp312-win_amd64.whl → 1.11.0__cp312-cp312-win_amd64.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.
@@ -0,0 +1,1133 @@
1
+ ###############################################################################
2
+ #
3
+ # (C) Copyright 2025 EVERYSK TECHNOLOGIES
4
+ #
5
+ # This is an unpublished work containing confidential and proprietary
6
+ # information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
7
+ # without authorization of EVERYSK TECHNOLOGIES is prohibited.
8
+ #
9
+ ###############################################################################
10
+ import contextlib
11
+ import re
12
+ from collections.abc import Callable, Iterator
13
+ from sys import maxsize as max_int
14
+ from types import GenericAlias, UnionType
15
+ from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, Union, get_args, get_origin
16
+ from urllib.parse import urlparse
17
+
18
+ from everysk.core.datetime import Date, DateTime
19
+ from everysk.core.exceptions import ReadonlyError, RequiredError
20
+ from everysk.utils import bool_convert
21
+
22
+ FT = TypeVar('FT')
23
+ if TYPE_CHECKING:
24
+ from everysk.core.mapping.base import BaseMapping
25
+
26
+
27
+ ## Internal functions
28
+ def _do_nothing(*args, **kwargs) -> None:
29
+ # pylint: disable=unused-argument
30
+ raise ReadonlyError('This field value cannot be changed.')
31
+
32
+
33
+ def get_field_type(field_type: Any) -> type:
34
+ """
35
+ Get the correct field type from the given field type.
36
+ We need to handle GenericAlias and UnionType to get the correct internal type.
37
+
38
+ Args:
39
+ field_type (FT): The field type to process.
40
+ """
41
+ if isinstance(field_type, GenericAlias):
42
+ # We need to get the origin of the generic type
43
+ field_type = get_origin(field_type)
44
+
45
+ # TypeError: typing.Union cannot be used with isinstance()
46
+ # The correct class of Unions is types.UnionType
47
+ elif isinstance(field_type, UnionType):
48
+ # We need to find if there are GenericAlias inside the Union
49
+ types = []
50
+ for _type in get_args(field_type):
51
+ if isinstance(_type, GenericAlias):
52
+ _type = get_origin(_type)
53
+ # We discard NoneType from the Union because it's normally used as optional
54
+ # This check is here because None is always the last type in the Union
55
+ if _type is type(None):
56
+ continue
57
+
58
+ types.append(_type)
59
+
60
+ # If there is only one type, we use it directly
61
+ if len(types) == 1:
62
+ field_type = types[0]
63
+ else:
64
+ field_type = Union[tuple(types)] # noqa: UP007
65
+
66
+ return field_type
67
+
68
+
69
+ class BaseMappingField(Generic[FT]):
70
+ # https://docs.python.org/3.12/howto/descriptor.html
71
+ choices: set[FT] | None = None
72
+ default: FT = Undefined
73
+ field_name: str | None = None
74
+ field_type: FT | None = None
75
+ readonly: bool = False
76
+ required: bool = False
77
+
78
+ ## Internal methods
79
+ def __init__(
80
+ self,
81
+ default: FT | None = Undefined,
82
+ *,
83
+ choices: set[FT] | None = Undefined,
84
+ readonly: bool = False,
85
+ required: bool = False,
86
+ **kwargs,
87
+ ) -> None:
88
+ if 'field_type' in kwargs:
89
+ self.field_type = kwargs.pop('field_type')
90
+
91
+ if 'field_name' in kwargs:
92
+ self.field_name = kwargs.pop('field_name')
93
+
94
+ if choices:
95
+ self.choices = choices
96
+ else:
97
+ self.choices = set()
98
+
99
+ # We always validate the default value
100
+ if default != self.default:
101
+ # Use class here to avoid issues with subclasses, BaseMapping as example
102
+ if default.__class__ in (dict, list, set) and not readonly:
103
+ # https://docs.astral.sh/ruff/rules/mutable-class-default/
104
+ # For default values they can't be mutable types unless the field is readonly
105
+ raise ValueError('Default value cannot be a dict, list, or set unless the field is readonly.')
106
+
107
+ default = self.clean_value(default)
108
+ self.validate(default)
109
+
110
+ self.default = default
111
+ ## These must be after default validation otherwise we can't set readonly fields with default values
112
+ ## and then we can't set required fields with default even NONE values
113
+ self.readonly = readonly
114
+ self.required = required
115
+
116
+ if kwargs:
117
+ for key, value in kwargs.items():
118
+ setattr(self, key, value)
119
+
120
+ def __delete__(self, obj: 'BaseMapping') -> None:
121
+ """
122
+ Delete the attribute/key from the instance.
123
+
124
+ Args:
125
+ obj (BaseMapping): The instance from which to delete the attribute.
126
+ """
127
+ if self.field_name in obj.__dict__:
128
+ del obj.__dict__[self.field_name]
129
+
130
+ if self.field_name in obj:
131
+ del obj[self.field_name]
132
+
133
+ obj.__deleted_keys__.add(self.field_name)
134
+
135
+ def __get__(self, obj: 'BaseMapping', cls: type) -> FT:
136
+ """
137
+ Get the attribute/key from the instance or from the class.
138
+
139
+ Args:
140
+ obj (BaseMapping): The instance from which to get the attribute.
141
+ cls (type): The class of the instance.
142
+
143
+ Raises:
144
+ AttributeError: If the attribute is not found in the instance.
145
+ """
146
+ # When obj is None, it means we are accessing the descriptor from the class, not from an instance
147
+ if obj is None:
148
+ return self.default
149
+
150
+ if self.field_name in obj:
151
+ return obj[self.field_name]
152
+
153
+ if self.field_name in obj.__dict__:
154
+ return obj.__dict__[self.field_name]
155
+
156
+ if self.field_name not in obj.__deleted_keys__:
157
+ return self.default
158
+
159
+ msg = f"'{cls.__name__}' object has no attribute '{self.field_name}'."
160
+ raise AttributeError(msg) from None
161
+
162
+ def __set__(self, obj: 'BaseMapping', value: FT) -> None:
163
+ """
164
+ Set the attribute/key in the instance.
165
+
166
+ Args:
167
+ obj (BaseMapping): The instance in which to set the attribute.
168
+ value (FT): The value to set for the attribute.
169
+ """
170
+ value = self.clean_value(value)
171
+ validate_fields = getattr(obj, '__validate_fields__', False)
172
+ if validate_fields:
173
+ self.validate(value)
174
+
175
+ if obj._is_valid_key(self.field_name): # noqa: SLF001
176
+ obj[self.field_name] = value
177
+
178
+ # This makes attribute inheritance work correctly
179
+ elif value != self.default:
180
+ obj.__dict__[self.field_name] = value
181
+
182
+ def __set_name__(self, cls: 'BaseMapping', name: str) -> None:
183
+ """
184
+ Set the name of the field in the descriptor.
185
+ It's called when the class is created in the Python runtime.
186
+
187
+ Args:
188
+ cls (BaseMapping): The class where the descriptor is used.
189
+ name (str): The name of the attribute in the class.
190
+ """
191
+ if self.field_name is None:
192
+ self.field_name = name
193
+
194
+ ## Private methods
195
+ def _get_choices(self) -> set[FT]:
196
+ """Get the choices for the field."""
197
+ return self.choices if self.choices is not None else set()
198
+
199
+ def _validate_choices(self, value: FT) -> None:
200
+ """
201
+ Validate that the value is one of the allowed choices.
202
+
203
+ Args:
204
+ value (FT): The value to validate.
205
+
206
+ Raises:
207
+ ValueError: If the value is not one of the allowed choices.
208
+ """
209
+ # None and Undefined are always allowed
210
+ if value is None or value is Undefined:
211
+ return
212
+
213
+ choices = self._get_choices()
214
+ msg = f"The value '{value}' for field '{self.field_name}' must be in this list {choices}."
215
+ if isinstance(value, set):
216
+ if value.difference(choices):
217
+ raise ValueError(msg)
218
+
219
+ return
220
+
221
+ if isinstance(value, (list, tuple)):
222
+ if set(value).difference(choices):
223
+ raise ValueError(msg)
224
+ return
225
+
226
+ if value not in choices:
227
+ raise ValueError(msg)
228
+
229
+ def _validate_instance(self, value: FT) -> None:
230
+ """
231
+ Validate that the value is an instance of the expected field type.
232
+
233
+ Args:
234
+ value (FT): The value to validate.
235
+
236
+ Raises:
237
+ TypeError: If the value is not an instance of the expected field type.
238
+ """
239
+ if value and not isinstance(value, self.field_type):
240
+ name = self.field_name
241
+ type_name = self.field_type.__qualname__
242
+ received_name = type(value).__qualname__
243
+ msg = f'Field {name} must be of type {type_name}, got {received_name}.'
244
+ raise TypeError(msg)
245
+
246
+ def _validate_max_value(self, value: FT, max_value: FT, msg: str | None = None) -> None:
247
+ """
248
+ Validate that the value is less than or equal to the maximum value.
249
+
250
+ Args:
251
+ value (FT): The value to validate.
252
+ max_value (FT): The maximum allowed value.
253
+ msg (str | None): Custom error message.
254
+
255
+ Raises:
256
+ ValueError: If the value is greater than the maximum allowed value.
257
+ """
258
+ if value is not None and value is not Undefined:
259
+ max_value = max_value if not callable(max_value) else max_value()
260
+ if msg is None:
261
+ msg = f"The value '{value}' for field '{self.field_name}' must be less than or equal to {max_value}."
262
+
263
+ if value > max_value:
264
+ raise ValueError(msg)
265
+
266
+ def _validate_min_value(self, value: FT, min_value: FT, msg: str | None = None) -> None:
267
+ """
268
+ Validate that the value is greater than or equal to the minimum value.
269
+
270
+ Args:
271
+ value (FT): The value to validate.
272
+ min_value (FT): The minimum allowed value.
273
+ msg (str | None): Custom error message.
274
+
275
+ Raises:
276
+ ValueError: If the value is less than the minimum allowed value.
277
+ """
278
+ if value is not None and value is not Undefined:
279
+ min_value = min_value if not callable(min_value) else min_value()
280
+ if msg is None:
281
+ msg = f"The value '{value}' for field '{self.field_name}' must be greater than or equal to {min_value}."
282
+
283
+ if value < min_value:
284
+ raise ValueError(msg)
285
+
286
+ def _validate_readonly(self, value: FT) -> None: # noqa: ARG002
287
+ """
288
+ Validate that the value is readonly.
289
+
290
+ Args:
291
+ value (FT): The value to validate.
292
+
293
+ Raises:
294
+ ValueError: If the value is readonly.
295
+ """
296
+ msg = f'Field {self.field_name} is readonly.'
297
+ raise ReadonlyError(msg)
298
+
299
+ def _validate_required(self, value: FT) -> None:
300
+ """
301
+ Validate that the value is required.
302
+
303
+ Args:
304
+ value (FT): The value to validate.
305
+
306
+ Raises:
307
+ ValueError: If the value is required.
308
+ """
309
+ if not value and value not in (0, 0.0, False):
310
+ msg = f'Field {self.field_name} is required.'
311
+ raise RequiredError(msg)
312
+
313
+ ## Public methods
314
+ def clean_value(self, value: object) -> FT:
315
+ """
316
+ Clean the value before setting it in the instance.
317
+ It can be overridden in subclasses to implement custom cleaning logic.
318
+ Like string to Date/DateTime objects, etc.
319
+
320
+ Args:
321
+ value (object): The value assigned to the field.
322
+ """
323
+ return value
324
+
325
+ def validate(self, value: object) -> None:
326
+ """
327
+ It's called to validate the value before setting it in the instance.
328
+ It can be overridden in subclasses to implement custom validation logic.
329
+
330
+ Args:
331
+ value (object): The value assigned to the field.
332
+
333
+ Raises:
334
+ ValueError: If the value is not valid.
335
+ """
336
+ # We need to validate required first because None and Undefined
337
+ # could be seen and are not valid choices for this validation
338
+ if self.required:
339
+ self._validate_required(value=value)
340
+
341
+ # We only validate if the value is different from default
342
+ # This allows us to set the default value in the initialization
343
+ if value != self.default:
344
+ if self.readonly:
345
+ self._validate_readonly(value=value)
346
+
347
+ if self.choices or self._get_choices():
348
+ self._validate_choices(value=value)
349
+
350
+ if self.field_type:
351
+ self._validate_instance(value=value)
352
+
353
+
354
+ ## StrField is here because other fields like ChoiceField/EmailField/URLField
355
+ ## inherit from it otherwise we keep alphabetical order
356
+ class StrField(BaseMappingField[str]):
357
+ field_type: type = str
358
+ min_size: int = 0
359
+ max_size: int = max_int
360
+ regex: str | None = None
361
+
362
+ def __init__(
363
+ self,
364
+ default: FT | None = Undefined,
365
+ *,
366
+ choices: set[FT] | None = Undefined,
367
+ min_size: int | None = None,
368
+ max_size: int | None = None,
369
+ regex: str | None = None,
370
+ readonly: bool = False,
371
+ required: bool = False,
372
+ **kwargs,
373
+ ) -> None:
374
+ if min_size is not None:
375
+ self.min_size = min_size
376
+
377
+ if max_size is not None:
378
+ self.max_size = max_size
379
+
380
+ if regex is not None:
381
+ self.regex = regex
382
+
383
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
384
+
385
+ def validate(self, value: object) -> None:
386
+ """
387
+ Validate the value against the regex pattern if provided.
388
+
389
+ Args:
390
+ value (object): The value assigned to the field.
391
+
392
+ Raises:
393
+ ValueError: If the value does not match the regex pattern.
394
+ """
395
+ super().validate(value)
396
+
397
+ if self.regex and isinstance(value, str) and not re.match(self.regex, value):
398
+ msg = f"The value '{value}' for field '{self.field_name}' must match with this regex: {self.regex}"
399
+ raise ValueError(msg)
400
+
401
+ if value is not None and value is not Undefined:
402
+ actual_size = len(value)
403
+ if self.min_size is not None:
404
+ msg = f"The size for field '{self.field_name}' must be greater than or equal to {self.min_size}."
405
+ self._validate_min_value(value=actual_size, min_value=self.min_size, msg=msg)
406
+
407
+ if self.max_size is not None:
408
+ msg = f"The size for field '{self.field_name}' must be less than or equal to {self.max_size}."
409
+ self._validate_max_value(value=actual_size, max_value=self.max_size, msg=msg)
410
+
411
+
412
+ class BoolField(BaseMappingField[bool]):
413
+ field_type: type = bool
414
+
415
+ def clean_value(self, value: object) -> bool:
416
+ """
417
+ Converts the given value to a boolean if possible using the 'everysk.utils.bool_convert' function.
418
+
419
+ Args:
420
+ value (object): The value assigned to the field.
421
+
422
+ Returns:
423
+ Any: The value converted to its boolean corresponding
424
+
425
+ Example:
426
+ >>> from everysk.core.mapping import BoolField
427
+ >>> bool_field = BoolField()
428
+ >>> bool_field.clean_value("y")
429
+ >>> True
430
+
431
+ >>> bool_field.clean_value("n")
432
+ >>> False
433
+
434
+ >>> bool_field.clean_value("a")
435
+ >>> ValueError: Invalid truth value 'a'
436
+ """
437
+ # https://docs.python.org/3.9/distutils/apiref.html#distutils.util.strtobool
438
+ # The module distutils is deprecated, then we put the function code inside our codebase
439
+ if value is not None and value is not Undefined:
440
+ # If the format is invalid, we let the validation handle the error
441
+ with contextlib.suppress(ValueError):
442
+ value = bool_convert(value)
443
+
444
+ return super().clean_value(value)
445
+
446
+
447
+ class ChoiceField(StrField):
448
+ # Legacy class for compatibility
449
+ pass
450
+
451
+
452
+ class DateField(BaseMappingField[Date]):
453
+ field_type: type = Date
454
+ min_date: Date | Callable = None
455
+ max_date: Date | Callable = None
456
+
457
+ def __init__(
458
+ self,
459
+ default: FT | None = Undefined,
460
+ *,
461
+ choices: set[FT] | None = Undefined,
462
+ min_date: Date | Callable = None,
463
+ max_date: Date | Callable = None,
464
+ readonly: bool = False,
465
+ required: bool = False,
466
+ **kwargs,
467
+ ) -> None:
468
+ if min_date is not None:
469
+ self.min_date = min_date
470
+
471
+ if max_date is not None:
472
+ self.max_date = max_date
473
+
474
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
475
+
476
+ def clean_value(self, value: object) -> Date:
477
+ """
478
+ Convert a string representation of a date into a Date object.
479
+
480
+ Args:
481
+ value (object): The value assigned to the field.
482
+
483
+ Raises:
484
+ ValueError: If the string is not represented in either
485
+ ISO format ("YYYY-MM-DD") or Everysk format ("YYYYMMDD").
486
+
487
+ Example:
488
+ >>> from everysk.core.fields import DateField
489
+ >>> date_field = DateField()
490
+ >>> date_field.clean_value("20140314")
491
+ Date(2014, 3, 14)
492
+ >>> date_field.clean_value("2014-03-14")
493
+ Date(2014, 3, 14)
494
+ """
495
+ if isinstance(value, str):
496
+ if '-' in value:
497
+ value = Date.fromisoformat(value)
498
+ else:
499
+ # Everysk format
500
+ # If the format is invalid, we let the validation handle the error
501
+ with contextlib.suppress(ValueError):
502
+ value = Date.strptime(value, '%Y%m%d')
503
+
504
+ return super().clean_value(value)
505
+
506
+ def validate(self, value: object) -> None:
507
+ """
508
+ Validate that the date is within the specified min_date and max_date if provided.
509
+
510
+ Args:
511
+ value (object): The value assigned to the field.
512
+
513
+ Raises:
514
+ ValueError: If the date is outside the specified range.
515
+ """
516
+ super().validate(value)
517
+
518
+ if self.min_date is not None:
519
+ self._validate_min_value(value=value, min_value=self.min_date)
520
+
521
+ if self.max_date is not None:
522
+ self._validate_max_value(value=value, max_value=self.max_date)
523
+
524
+
525
+ class DateTimeField(BaseMappingField[DateTime]):
526
+ field_type: type = DateTime
527
+ force_time: Literal['MIDDAY', 'NOW', 'FIRST_MINUTE', 'LAST_MINUTE'] = None
528
+ min_date: DateTime | Callable = None
529
+ max_date: DateTime | Callable = None
530
+
531
+ def __init__(
532
+ self,
533
+ default: FT | None = Undefined,
534
+ *,
535
+ choices: set[FT] | None = Undefined,
536
+ force_time: Literal['MIDDAY', 'NOW', 'FIRST_MINUTE', 'LAST_MINUTE'] | None = None,
537
+ min_date: DateTime | Callable = None,
538
+ max_date: DateTime | Callable = None,
539
+ readonly: bool = False,
540
+ required: bool = False,
541
+ **kwargs,
542
+ ) -> None:
543
+ if force_time is not None:
544
+ self.force_time = force_time
545
+
546
+ if min_date is not None:
547
+ self.min_date = min_date
548
+
549
+ if max_date is not None:
550
+ self.max_date = max_date
551
+
552
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
553
+
554
+ def clean_value(self, value: object) -> DateTime:
555
+ """
556
+ Convert a string representation of a datetime into a DateTime object.
557
+
558
+ Args:
559
+ value (object): The value assigned to the field.
560
+
561
+ Raises:
562
+ ValueError: If the string is not represented in either
563
+ ISO format ("YYYY-MM-DDTHH:MM:SS") or Everysk format ("YYYYMMDD HHMMSS").
564
+
565
+ Example:
566
+ >>> from everysk.core.fields import DateTimeField
567
+ >>> datetime_field = DateTimeField()
568
+ >>> datetime_field.clean_value("20140314 153000")
569
+ DateTime(2014, 3, 14, 15, 30, 0)
570
+ >>> datetime_field.clean_value("2014-03-14T15:30:00")
571
+ DateTime(2014, 3, 14, 15, 30, 0)
572
+ """
573
+ if isinstance(value, str):
574
+ # If the format is invalid, we let the validation handle the error
575
+ with contextlib.suppress(ValueError):
576
+ value: DateTime = DateTime.fromisoformat(value)
577
+
578
+ elif Date.is_date(value):
579
+ value: DateTime = DateTime.fromisoformat(value.isoformat())
580
+
581
+ if self.force_time and DateTime.is_datetime(value):
582
+ value = value.force_time(self.force_time)
583
+
584
+ return super().clean_value(value)
585
+
586
+ def validate(self, value: object) -> None:
587
+ """
588
+ Validate that the datetime is within the specified min_date and max_date if provided.
589
+
590
+ Args:
591
+ value (object): The value assigned to the field.
592
+
593
+ Raises:
594
+ ValueError: If the datetime is outside the specified range.
595
+ """
596
+ super().validate(value)
597
+
598
+ if self.min_date is not None:
599
+ self._validate_min_value(value=value, min_value=self.min_date)
600
+
601
+ if self.max_date is not None:
602
+ self._validate_max_value(value=value, max_value=self.max_date)
603
+
604
+
605
+ class DictField(BaseMappingField[dict]):
606
+ field_type: type = dict
607
+
608
+ class ReadonlyDict(dict):
609
+ __setitem__ = _do_nothing
610
+ __delitem__ = _do_nothing
611
+ pop = _do_nothing
612
+ popitem = _do_nothing
613
+ clear = _do_nothing
614
+ update = _do_nothing
615
+ setdefault = _do_nothing
616
+
617
+ def clean_value(self, value: object) -> dict | ReadonlyDict:
618
+ """
619
+ If the field is readonly, convert the given dict into a ReadonlyDict.
620
+
621
+ Args:
622
+ value (object): The value assigned to the field.
623
+ """
624
+ if self.readonly and isinstance(value, dict):
625
+ value = self.ReadonlyDict(value)
626
+
627
+ return super().clean_value(value)
628
+
629
+
630
+ class EmailField(StrField):
631
+ field_type: type = str
632
+ min_size: int = 5
633
+ max_size: int = 320
634
+
635
+ def validate(self, value: object) -> None:
636
+ """
637
+ Validates if the value is an e-mail address.
638
+ To validate we check the existence of the '@' character and the length of the string.
639
+
640
+ Args:
641
+ value (object): The value assigned to the field.
642
+
643
+ Raises:
644
+ ValueError: If the value is not an e-mail address.
645
+ """
646
+ super().validate(value)
647
+
648
+ # The maximum length of an email is 320 characters per RFC 3696 section 3 and at least 5 digits a@b.c
649
+ msg = f'Key {self.field_name} must be an e-mail.'
650
+ if value is not None and value is not Undefined:
651
+ if '@' not in value:
652
+ raise ValueError(msg)
653
+
654
+ try:
655
+ # Get the cases where we could have more than one @ in the string
656
+ user, domain = value.split('@') # noqa: RUF059
657
+ except ValueError as error:
658
+ raise ValueError(msg) from error
659
+
660
+
661
+ class FloatField(BaseMappingField[float]):
662
+ field_type: type = float
663
+ min_value: float = float('-inf')
664
+ max_value: float = float('inf')
665
+
666
+ def __init__(
667
+ self,
668
+ default: FT | None = Undefined,
669
+ *,
670
+ choices: set[FT] | None = Undefined,
671
+ min_value: float | None = None,
672
+ max_value: float | None = None,
673
+ readonly: bool = False,
674
+ required: bool = False,
675
+ **kwargs,
676
+ ) -> None:
677
+ if min_value is not None:
678
+ self.min_value = min_value
679
+
680
+ if max_value is not None:
681
+ self.max_value = max_value
682
+
683
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
684
+
685
+ def clean_value(self, value: object) -> float:
686
+ """
687
+ Convert a string or integer representation of a float into a float object.
688
+
689
+ Args:
690
+ value (object): The value assigned to the field.
691
+
692
+ Raises:
693
+ ValueError: If value could not be converted to a float.
694
+ """
695
+ # Convert Float strings to float object
696
+ if isinstance(value, (int, str)):
697
+ # If the format is invalid, we let the validation handle the error
698
+ with contextlib.suppress(ValueError):
699
+ value = float(value)
700
+
701
+ return super().clean_value(value)
702
+
703
+ def validate(self, value: object) -> None:
704
+ """
705
+ Validate that the float value is within the specified min_value and max_value.
706
+
707
+ Args:
708
+ value (object): The value assigned to the field.
709
+
710
+ Raises:
711
+ ValueError: If the float value is outside the specified range.
712
+ """
713
+ super().validate(value)
714
+
715
+ if self.min_value is not None:
716
+ self._validate_min_value(value=value, min_value=self.min_value)
717
+
718
+ if self.max_value is not None:
719
+ self._validate_max_value(value=value, max_value=self.max_value)
720
+
721
+
722
+ class IntField(BaseMappingField[int]):
723
+ field_type: type = int
724
+ min_value: int = -max_int - 1
725
+ max_value: int = max_int
726
+
727
+ def __init__(
728
+ self,
729
+ default: FT | None = Undefined,
730
+ *,
731
+ choices: set[FT] | None = Undefined,
732
+ min_value: int | None = None,
733
+ max_value: int | None = None,
734
+ readonly: bool = False,
735
+ required: bool = False,
736
+ **kwargs,
737
+ ) -> None:
738
+ if min_value is not None:
739
+ self.min_value = min_value
740
+
741
+ if max_value is not None:
742
+ self.max_value = max_value
743
+
744
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
745
+
746
+ def clean_value(self, value: object) -> int:
747
+ """
748
+ Convert a string or float representation of an integer into an int object.
749
+
750
+ Args:
751
+ value (object): The value assigned to the field.
752
+
753
+ Raises:
754
+ ValueError: If value could not be converted to an int.
755
+ """
756
+ # Convert Int strings to int object
757
+ if isinstance(value, str):
758
+ # If the format is invalid, we let the validation handle the error
759
+ with contextlib.suppress(ValueError):
760
+ value = int(value)
761
+
762
+ return super().clean_value(value)
763
+
764
+ def validate(self, value: object) -> None:
765
+ """
766
+ Validate that the integer value is within the specified min_value and max_value.
767
+
768
+ Args:
769
+ value (object): The value assigned to the field.
770
+
771
+ Raises:
772
+ ValueError: If the integer value is outside the specified range.
773
+ """
774
+ super().validate(value)
775
+
776
+ if self.min_value is not None:
777
+ self._validate_min_value(value=value, min_value=self.min_value)
778
+
779
+ if self.max_value is not None:
780
+ self._validate_max_value(value=value, max_value=self.max_value)
781
+
782
+
783
+ class IteratorField(BaseMappingField[Iterator]):
784
+ field_type: type = Iterator
785
+ separator: str = ','
786
+
787
+ def __init__(
788
+ self,
789
+ default: FT | None = Undefined,
790
+ *,
791
+ choices: set[FT] | None = Undefined,
792
+ readonly: bool = False,
793
+ required: bool = False,
794
+ separator: str = ',',
795
+ **kwargs,
796
+ ) -> None:
797
+ self.separator = separator
798
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
799
+
800
+ def clean_value(self, value: object) -> Iterator:
801
+ # Convert List/Set/Tuple to iterator
802
+ if isinstance(value, (list, set, tuple)):
803
+ value = iter(value)
804
+
805
+ elif isinstance(value, str) and self.separator in value:
806
+ value = iter([item.strip() for item in value.split(self.separator)])
807
+
808
+ return super().clean_value(value)
809
+
810
+
811
+ class ListField(BaseMappingField[list]):
812
+ """
813
+ https://stackoverflow.com/questions/855191/how-big-can-a-python-list-get#comment112727918_15739630
814
+ Sys.maxsize should give you 2^31 - 1 on a 32-bit platform and 2^63 - 1 on a 64-bit platform.
815
+ However, because each pointer on the 32 bit list takes up 4 bytes or on a 64 bit it's 8 bytes,
816
+ Python would give you an error if you attempt to make a list larger than maxsize/8 on a 64-bit
817
+ system or maxsize/4 on a 32-bit system.
818
+
819
+ Example:
820
+ >>> import sys
821
+ >>> sys.maxsize
822
+ 9223372036854775807
823
+ >>> sys.maxsize // 8
824
+ 1152921504606846975 # Maximum list size on a 64-bit system that is pretty much impossible to reach
825
+ """
826
+
827
+ field_type: type = list
828
+ min_size: int = 0
829
+ max_size: int = max_int // 8
830
+ separator: str = ','
831
+
832
+ class ReadonlyList(list):
833
+ append = _do_nothing
834
+ clear = _do_nothing
835
+ extend = _do_nothing
836
+ insert = _do_nothing
837
+ pop = _do_nothing
838
+ remove = _do_nothing
839
+ reverse = _do_nothing
840
+ sort = _do_nothing
841
+
842
+ def __init__(
843
+ self,
844
+ default: FT | None = Undefined,
845
+ *,
846
+ choices: set[FT] | None = Undefined,
847
+ min_size: int | None = None,
848
+ max_size: int | None = None,
849
+ readonly: bool = False,
850
+ required: bool = False,
851
+ separator: str = ',',
852
+ **kwargs,
853
+ ) -> None:
854
+ if min_size is not None:
855
+ self.min_size = min_size
856
+
857
+ if max_size is not None:
858
+ self.max_size = max_size
859
+
860
+ self.separator = separator
861
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
862
+
863
+ def clean_value(self, value: object) -> list | ReadonlyList:
864
+ """
865
+ Convert Tuple/Set to List.
866
+ Convert a string that represents a list into a list object using the defined separator.
867
+ If the field is readonly, convert the given list into a ReadonlyList.
868
+
869
+ Args:
870
+ value (object): The value assigned to the field.
871
+ """
872
+ if isinstance(value, (tuple, set)):
873
+ value = list(value)
874
+
875
+ elif isinstance(value, str) and self.separator in value:
876
+ value = [item.strip() for item in value.split(self.separator)]
877
+
878
+ if self.readonly and isinstance(value, list):
879
+ value = self.ReadonlyList(value)
880
+
881
+ return super().clean_value(value)
882
+
883
+ def validate(self, value: object) -> None:
884
+ """
885
+ Validate that the list size is within the specified min_size and max_size.
886
+
887
+ Args:
888
+ value (object): The value assigned to the field.
889
+
890
+ Raises:
891
+ ValueError: If the list size is outside the specified range.
892
+ """
893
+ super().validate(value)
894
+
895
+ if value is not None and value is not Undefined:
896
+ actual_size = len(value)
897
+ if self.min_size is not None:
898
+ msg = f"The size for field '{self.field_name}' must be greater than or equal to {self.min_size}."
899
+ self._validate_min_value(value=actual_size, min_value=self.min_size, msg=msg)
900
+
901
+ if self.max_size is not None:
902
+ msg = f"The size for field '{self.field_name}' must be less than or equal to {self.max_size}."
903
+ self._validate_max_value(value=actual_size, max_value=self.max_size, msg=msg)
904
+
905
+
906
+ class SetField(BaseMappingField[set]):
907
+ field_type: type = set
908
+ min_size: int = 0
909
+ max_size: int = max_int // 8
910
+ separator: str = ','
911
+
912
+ class ReadonlySet(set):
913
+ add = _do_nothing
914
+ clear = _do_nothing
915
+ discard = _do_nothing
916
+ pop = _do_nothing
917
+ remove = _do_nothing
918
+ update = _do_nothing
919
+
920
+ def __init__(
921
+ self,
922
+ default: FT | None = Undefined,
923
+ *,
924
+ choices: set[FT] | None = Undefined,
925
+ min_size: int | None = None,
926
+ max_size: int | None = None,
927
+ readonly: bool = False,
928
+ required: bool = False,
929
+ separator: str = ',',
930
+ **kwargs,
931
+ ) -> None:
932
+ if min_size is not None:
933
+ self.min_size = min_size
934
+
935
+ if max_size is not None:
936
+ self.max_size = max_size
937
+
938
+ self.separator = separator
939
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
940
+
941
+ def clean_value(self, value: object) -> set | ReadonlySet:
942
+ """
943
+ Convert List/Tuple to Set.
944
+ Convert a string that represents a set into a set object using the defined separator.
945
+ If the field is readonly, convert the given set into a ReadonlySet.
946
+
947
+ Args:
948
+ value (object): The value assigned to the field.
949
+ """
950
+ if isinstance(value, (list, tuple)):
951
+ value = set(value)
952
+
953
+ elif isinstance(value, str) and self.separator in value:
954
+ value = {item.strip() for item in value.split(self.separator)}
955
+
956
+ if self.readonly and isinstance(value, set):
957
+ value = self.ReadonlySet(value)
958
+
959
+ return super().clean_value(value)
960
+
961
+ def validate(self, value: object) -> None:
962
+ """
963
+ Validate that the set size is within the specified min_size and max_size.
964
+
965
+ Args:
966
+ value (object): The value assigned to the field.
967
+
968
+ Raises:
969
+ ValueError: If the set size is outside the specified range.
970
+ """
971
+ super().validate(value)
972
+
973
+ if value is not None and value is not Undefined:
974
+ actual_size = len(value)
975
+ if self.min_size is not None:
976
+ msg = f"The size for field '{self.field_name}' must be greater than or equal to {self.min_size}."
977
+ self._validate_min_value(value=actual_size, min_value=self.min_size, msg=msg)
978
+
979
+ if self.max_size is not None:
980
+ msg = f"The size for field '{self.field_name}' must be less than or equal to {self.max_size}."
981
+ self._validate_max_value(value=actual_size, max_value=self.max_size, msg=msg)
982
+
983
+
984
+ class RegexField(BaseMappingField[re.Pattern]):
985
+ field_type: type = re.Pattern
986
+
987
+ def clean_value(self, value: object) -> re.Pattern:
988
+ """
989
+ Compile the given string into a regex Pattern object.
990
+
991
+ Args:
992
+ value (object): The value assigned to the field.
993
+ """
994
+ if isinstance(value, str):
995
+ value = re.compile(value)
996
+
997
+ return super().clean_value(value)
998
+
999
+
1000
+ class TupleField(BaseMappingField[tuple]):
1001
+ field_type: type = tuple
1002
+ min_size: int = 0
1003
+ max_size: int = max_int // 8
1004
+ separator: str = ','
1005
+
1006
+ def __init__(
1007
+ self,
1008
+ default: FT | None = Undefined,
1009
+ *,
1010
+ choices: set[FT] | None = Undefined,
1011
+ min_size: int | None = None,
1012
+ max_size: int | None = None,
1013
+ readonly: bool = False,
1014
+ required: bool = False,
1015
+ separator: str = ',',
1016
+ **kwargs,
1017
+ ) -> None:
1018
+ if min_size is not None:
1019
+ self.min_size = min_size
1020
+
1021
+ if max_size is not None:
1022
+ self.max_size = max_size
1023
+
1024
+ self.separator = separator
1025
+ super().__init__(default=default, choices=choices, readonly=readonly, required=required, **kwargs)
1026
+
1027
+ def clean_value(self, value: object) -> tuple:
1028
+ """
1029
+ Convert a list or set representation of a tuple into a tuple object.
1030
+ Convert a string that represents a tuple into a tuple object using the defined separator.
1031
+
1032
+ Args:
1033
+ value (object): The value assigned to the field.
1034
+ """
1035
+ if isinstance(value, (list, set)):
1036
+ value = tuple(value)
1037
+ elif isinstance(value, str) and self.separator in value:
1038
+ value = tuple(item.strip() for item in value.split(self.separator))
1039
+
1040
+ return super().clean_value(value)
1041
+
1042
+ def validate(self, value: object) -> None:
1043
+ """
1044
+ Validate that the tuple size is within the specified min_size and max_size.
1045
+
1046
+ Args:
1047
+ value (object): The value assigned to the field.
1048
+
1049
+ Raises:
1050
+ ValueError: If the tuple size is outside the specified range.
1051
+ """
1052
+ super().validate(value)
1053
+
1054
+ if value is not None and value is not Undefined:
1055
+ actual_size = len(value)
1056
+ if self.min_size is not None:
1057
+ msg = f"The size for field '{self.field_name}' must be greater than or equal to {self.min_size}."
1058
+ self._validate_min_value(value=actual_size, min_value=self.min_size, msg=msg)
1059
+
1060
+ if self.max_size is not None:
1061
+ msg = f"The size for field '{self.field_name}' must be less than or equal to {self.max_size}."
1062
+ self._validate_max_value(value=actual_size, max_value=self.max_size, msg=msg)
1063
+
1064
+
1065
+ class URLField(StrField):
1066
+ field_type: type = str
1067
+ supported_schemes: set = ('http', 'https', 'ftp', 'ftps', 'git')
1068
+
1069
+ def validate(self, value: object) -> None:
1070
+ """
1071
+ Validates a URL string to ensure it is a valid URL.
1072
+ We check the protocol HTTP, HTTPS, FTP, FTPS, the domain format and size and the port number.
1073
+
1074
+ Args:
1075
+ self.field_name (str): The name of the attribute being validated, used for identification in error messages.
1076
+ value (Any): The value to be validated for the specified attribute.
1077
+ attr_type (type | UnionType, optional):
1078
+ The expected type of the attribute. If provided, the value is checked to ensure it is of this type.
1079
+ Defaults to None.
1080
+
1081
+ Raises:
1082
+ FieldValueError: If the url is invalid.
1083
+ """
1084
+ super().validate(value)
1085
+
1086
+ if isinstance(value, str):
1087
+ # https://github.com/django/django/blob/main/django/core/validators.py
1088
+ ul = '\u00a1-\uffff' # Unicode letters range (must not be a raw string).
1089
+
1090
+ # IP patterns
1091
+ ipv4_re = (
1092
+ r'(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)'
1093
+ r'(?:\.(?:0|25[0-5]|2[0-4][0-9]|1[0-9]?[0-9]?|[1-9][0-9]?)){3}'
1094
+ )
1095
+ ipv6_re = r'\[[0-9a-f:.]+\]' # (simple regex, validated later)
1096
+
1097
+ # Host patterns
1098
+ hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
1099
+
1100
+ # Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
1101
+ domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*'
1102
+ tld_re = (
1103
+ r'\.' # dot
1104
+ r'(?!-)' # can't start with a dash
1105
+ r'(?:[a-z' + ul + '-]{2,63}' # domain label
1106
+ r'|xn--[a-z0-9]{1,59})' # or punycode label
1107
+ r'(?<!-)' # can't end with a dash
1108
+ r'\.?' # may have a trailing dot
1109
+ )
1110
+ host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'
1111
+
1112
+ regex = re.compile(
1113
+ r'^(?:[a-z0-9.+-]*)://' # scheme is validated separately
1114
+ r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?' # user:pass authentication
1115
+ r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
1116
+ r'(?::[0-9]{1,5})?' # port
1117
+ r'(?:[/?#][^\s]*)?' # resource path
1118
+ r'\Z',
1119
+ re.IGNORECASE,
1120
+ )
1121
+ msg = f'Key {self.field_name} must be an URL.'
1122
+ if not regex.match(value):
1123
+ raise ValueError(msg)
1124
+
1125
+ # The scheme needs a separate check
1126
+ try:
1127
+ result = urlparse(value)
1128
+
1129
+ except ValueError as error:
1130
+ raise ValueError(msg) from error
1131
+
1132
+ if result.scheme not in self.supported_schemes:
1133
+ raise ValueError(msg)