everysk-lib 1.10.3__cp312-cp312-macosx_11_0_arm64.whl → 1.11.0__cp312-cp312-macosx_11_0_arm64.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.
- everysk/core/_tests/mapping/__init__.py +9 -0
- everysk/core/_tests/mapping/base.py +306 -0
- everysk/core/_tests/mapping/fields.py +1154 -0
- everysk/core/datetime/datetime.py +1 -1
- everysk/core/http.py +6 -6
- everysk/core/mapping/__init__.py +29 -0
- everysk/core/mapping/base.py +214 -0
- everysk/core/mapping/fields.py +1133 -0
- everysk/core/mapping/metaclass.py +101 -0
- everysk/core/object.py +132 -124
- everysk/core/tests.py +19 -0
- everysk/sdk/entities/script.py +0 -1
- everysk/sql/connection.py +24 -0
- {everysk_lib-1.10.3.dist-info → everysk_lib-1.11.0.dist-info}/METADATA +2 -2
- {everysk_lib-1.10.3.dist-info → everysk_lib-1.11.0.dist-info}/RECORD +19 -12
- {everysk_lib-1.10.3.dist-info → everysk_lib-1.11.0.dist-info}/.gitignore +0 -0
- {everysk_lib-1.10.3.dist-info → everysk_lib-1.11.0.dist-info}/WHEEL +0 -0
- {everysk_lib-1.10.3.dist-info → everysk_lib-1.11.0.dist-info}/licenses/LICENSE.txt +0 -0
- {everysk_lib-1.10.3.dist-info → everysk_lib-1.11.0.dist-info}/top_level.txt +0 -0
|
@@ -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)
|