createsonline 0.1.26__py3-none-any.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.
- createsonline/__init__.py +46 -0
- createsonline/admin/__init__.py +7 -0
- createsonline/admin/content.py +526 -0
- createsonline/admin/crud.py +805 -0
- createsonline/admin/field_builder.py +559 -0
- createsonline/admin/integration.py +482 -0
- createsonline/admin/interface.py +2562 -0
- createsonline/admin/model_creator.py +513 -0
- createsonline/admin/model_manager.py +388 -0
- createsonline/admin/modern_dashboard.py +498 -0
- createsonline/admin/permissions.py +264 -0
- createsonline/admin/user_forms.py +594 -0
- createsonline/ai/__init__.py +202 -0
- createsonline/ai/fields.py +1226 -0
- createsonline/ai/orm.py +325 -0
- createsonline/ai/services.py +1244 -0
- createsonline/app.py +506 -0
- createsonline/auth/__init__.py +8 -0
- createsonline/auth/management.py +228 -0
- createsonline/auth/models.py +552 -0
- createsonline/cli/__init__.py +5 -0
- createsonline/cli/commands/__init__.py +122 -0
- createsonline/cli/commands/database.py +416 -0
- createsonline/cli/commands/info.py +173 -0
- createsonline/cli/commands/initdb.py +218 -0
- createsonline/cli/commands/project.py +545 -0
- createsonline/cli/commands/serve.py +173 -0
- createsonline/cli/commands/shell.py +93 -0
- createsonline/cli/commands/users.py +148 -0
- createsonline/cli/main.py +2041 -0
- createsonline/cli/manage.py +274 -0
- createsonline/config/__init__.py +9 -0
- createsonline/config/app.py +2577 -0
- createsonline/config/database.py +179 -0
- createsonline/config/docs.py +384 -0
- createsonline/config/errors.py +160 -0
- createsonline/config/orm.py +43 -0
- createsonline/config/request.py +93 -0
- createsonline/config/settings.py +176 -0
- createsonline/data/__init__.py +23 -0
- createsonline/data/dataframe.py +925 -0
- createsonline/data/io.py +453 -0
- createsonline/data/series.py +557 -0
- createsonline/database/__init__.py +60 -0
- createsonline/database/abstraction.py +440 -0
- createsonline/database/assistant.py +585 -0
- createsonline/database/fields.py +442 -0
- createsonline/database/migrations.py +132 -0
- createsonline/database/models.py +604 -0
- createsonline/database.py +438 -0
- createsonline/http/__init__.py +28 -0
- createsonline/http/client.py +535 -0
- createsonline/ml/__init__.py +55 -0
- createsonline/ml/classification.py +552 -0
- createsonline/ml/clustering.py +680 -0
- createsonline/ml/metrics.py +542 -0
- createsonline/ml/neural.py +560 -0
- createsonline/ml/preprocessing.py +784 -0
- createsonline/ml/regression.py +501 -0
- createsonline/performance/__init__.py +19 -0
- createsonline/performance/cache.py +444 -0
- createsonline/performance/compression.py +335 -0
- createsonline/performance/core.py +419 -0
- createsonline/project_init.py +789 -0
- createsonline/routing.py +528 -0
- createsonline/security/__init__.py +34 -0
- createsonline/security/core.py +811 -0
- createsonline/security/encryption.py +349 -0
- createsonline/server.py +295 -0
- createsonline/static/css/admin.css +263 -0
- createsonline/static/css/common.css +358 -0
- createsonline/static/css/dashboard.css +89 -0
- createsonline/static/favicon.ico +0 -0
- createsonline/static/icons/icon-128x128.png +0 -0
- createsonline/static/icons/icon-128x128.webp +0 -0
- createsonline/static/icons/icon-16x16.png +0 -0
- createsonline/static/icons/icon-16x16.webp +0 -0
- createsonline/static/icons/icon-180x180.png +0 -0
- createsonline/static/icons/icon-180x180.webp +0 -0
- createsonline/static/icons/icon-192x192.png +0 -0
- createsonline/static/icons/icon-192x192.webp +0 -0
- createsonline/static/icons/icon-256x256.png +0 -0
- createsonline/static/icons/icon-256x256.webp +0 -0
- createsonline/static/icons/icon-32x32.png +0 -0
- createsonline/static/icons/icon-32x32.webp +0 -0
- createsonline/static/icons/icon-384x384.png +0 -0
- createsonline/static/icons/icon-384x384.webp +0 -0
- createsonline/static/icons/icon-48x48.png +0 -0
- createsonline/static/icons/icon-48x48.webp +0 -0
- createsonline/static/icons/icon-512x512.png +0 -0
- createsonline/static/icons/icon-512x512.webp +0 -0
- createsonline/static/icons/icon-64x64.png +0 -0
- createsonline/static/icons/icon-64x64.webp +0 -0
- createsonline/static/image/android-chrome-192x192.png +0 -0
- createsonline/static/image/android-chrome-512x512.png +0 -0
- createsonline/static/image/apple-touch-icon.png +0 -0
- createsonline/static/image/favicon-16x16.png +0 -0
- createsonline/static/image/favicon-32x32.png +0 -0
- createsonline/static/image/favicon.ico +0 -0
- createsonline/static/image/favicon.svg +17 -0
- createsonline/static/image/icon-128x128.png +0 -0
- createsonline/static/image/icon-128x128.webp +0 -0
- createsonline/static/image/icon-16x16.png +0 -0
- createsonline/static/image/icon-16x16.webp +0 -0
- createsonline/static/image/icon-180x180.png +0 -0
- createsonline/static/image/icon-180x180.webp +0 -0
- createsonline/static/image/icon-192x192.png +0 -0
- createsonline/static/image/icon-192x192.webp +0 -0
- createsonline/static/image/icon-256x256.png +0 -0
- createsonline/static/image/icon-256x256.webp +0 -0
- createsonline/static/image/icon-32x32.png +0 -0
- createsonline/static/image/icon-32x32.webp +0 -0
- createsonline/static/image/icon-384x384.png +0 -0
- createsonline/static/image/icon-384x384.webp +0 -0
- createsonline/static/image/icon-48x48.png +0 -0
- createsonline/static/image/icon-48x48.webp +0 -0
- createsonline/static/image/icon-512x512.png +0 -0
- createsonline/static/image/icon-512x512.webp +0 -0
- createsonline/static/image/icon-64x64.png +0 -0
- createsonline/static/image/icon-64x64.webp +0 -0
- createsonline/static/image/logo-header-h100.png +0 -0
- createsonline/static/image/logo-header-h100.webp +0 -0
- createsonline/static/image/logo-header-h200@2x.png +0 -0
- createsonline/static/image/logo-header-h200@2x.webp +0 -0
- createsonline/static/image/logo.png +0 -0
- createsonline/static/js/admin.js +274 -0
- createsonline/static/site.webmanifest +35 -0
- createsonline/static/templates/admin/base.html +87 -0
- createsonline/static/templates/admin/dashboard.html +217 -0
- createsonline/static/templates/admin/model_form.html +270 -0
- createsonline/static/templates/admin/model_list.html +202 -0
- createsonline/static/test_script.js +15 -0
- createsonline/static/test_styles.css +59 -0
- createsonline/static_files.py +365 -0
- createsonline/templates/404.html +100 -0
- createsonline/templates/admin_login.html +169 -0
- createsonline/templates/base.html +102 -0
- createsonline/templates/index.html +151 -0
- createsonline/templates.py +205 -0
- createsonline/testing.py +322 -0
- createsonline/utils.py +448 -0
- createsonline/validation/__init__.py +49 -0
- createsonline/validation/fields.py +598 -0
- createsonline/validation/models.py +504 -0
- createsonline/validation/validators.py +561 -0
- createsonline/views.py +184 -0
- createsonline-0.1.26.dist-info/METADATA +46 -0
- createsonline-0.1.26.dist-info/RECORD +152 -0
- createsonline-0.1.26.dist-info/WHEEL +5 -0
- createsonline-0.1.26.dist-info/entry_points.txt +2 -0
- createsonline-0.1.26.dist-info/licenses/LICENSE +21 -0
- createsonline-0.1.26.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CREATESONLINE Validation Fields
|
|
3
|
+
|
|
4
|
+
Pure Python field validation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime, date
|
|
10
|
+
from .validators import ValidationError
|
|
11
|
+
|
|
12
|
+
# Handle typing compatibility
|
|
13
|
+
if sys.version_info >= (3, 9):
|
|
14
|
+
from typing import Any, Optional, List, Dict, Union, Callable
|
|
15
|
+
else:
|
|
16
|
+
# Fallback for older Python versions (shouldn't happen since we require 3.9+)
|
|
17
|
+
from typing import Any, List, Dict, Union, Callable
|
|
18
|
+
Optional = Union
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Field:
|
|
22
|
+
"""
|
|
23
|
+
Base validation field
|
|
24
|
+
|
|
25
|
+
Pure Python field validation with custom validators.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
default: Any = None,
|
|
31
|
+
required: bool = True,
|
|
32
|
+
validators: Optional[List[Callable[[Any], bool]]] = None,
|
|
33
|
+
description: Optional[str] = None,
|
|
34
|
+
alias: Optional[str] = None
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Initialize field
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
default: Default value if not provided
|
|
41
|
+
required: Whether field is required
|
|
42
|
+
validators: List of validator functions
|
|
43
|
+
description: Field description
|
|
44
|
+
alias: Alternative field name
|
|
45
|
+
"""
|
|
46
|
+
self.default = default
|
|
47
|
+
self.required = required
|
|
48
|
+
self.validators = validators or []
|
|
49
|
+
self.description = description
|
|
50
|
+
self.alias = alias
|
|
51
|
+
|
|
52
|
+
# Add required validator if field is required
|
|
53
|
+
if self.required and default is None:
|
|
54
|
+
self.validators.insert(0, self._required_validator)
|
|
55
|
+
|
|
56
|
+
def _required_validator(self, value: Any) -> bool:
|
|
57
|
+
"""Check if value is not None/empty"""
|
|
58
|
+
if value is None:
|
|
59
|
+
raise ValidationError("Field is required")
|
|
60
|
+
if isinstance(value, (str, list, dict)) and len(value) == 0:
|
|
61
|
+
raise ValidationError("Field cannot be empty")
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def validate(self, value: Any, field_name: str = "field") -> Any:
|
|
65
|
+
"""
|
|
66
|
+
Validate field value
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
value: Value to validate
|
|
70
|
+
field_name: Name of the field for error messages
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Validated and converted value
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValidationError: If validation fails
|
|
77
|
+
"""
|
|
78
|
+
# Handle None values
|
|
79
|
+
if value is None:
|
|
80
|
+
if not self.required and self.default is not None:
|
|
81
|
+
return self.default
|
|
82
|
+
elif not self.required:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# Type conversion
|
|
86
|
+
try:
|
|
87
|
+
value = self.convert_type(value)
|
|
88
|
+
except (ValueError, TypeError) as e:
|
|
89
|
+
raise ValidationError(f"Invalid type for {field_name}: {e}")
|
|
90
|
+
|
|
91
|
+
# Run validators
|
|
92
|
+
for validator in self.validators:
|
|
93
|
+
try:
|
|
94
|
+
if not validator(value):
|
|
95
|
+
raise ValidationError(f"Validation failed for {field_name}")
|
|
96
|
+
except ValidationError:
|
|
97
|
+
raise
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise ValidationError(f"Validator error for {field_name}: {e}")
|
|
100
|
+
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
def convert_type(self, value: Any) -> Any:
|
|
104
|
+
"""Convert value to appropriate type (override in subclasses)"""
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
108
|
+
"""Convert field definition to dictionary"""
|
|
109
|
+
return {
|
|
110
|
+
'type': self.__class__.__name__,
|
|
111
|
+
'default': self.default,
|
|
112
|
+
'required': self.required,
|
|
113
|
+
'description': self.description,
|
|
114
|
+
'alias': self.alias
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class StringField(Field):
|
|
119
|
+
"""String field with length validation"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
min_length: Optional[int] = None,
|
|
124
|
+
max_length: Optional[int] = None,
|
|
125
|
+
pattern: Optional[str] = None,
|
|
126
|
+
**kwargs
|
|
127
|
+
):
|
|
128
|
+
"""
|
|
129
|
+
Initialize string field
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
min_length: Minimum string length
|
|
133
|
+
max_length: Maximum string length
|
|
134
|
+
pattern: Regex pattern to match
|
|
135
|
+
**kwargs: Base field arguments
|
|
136
|
+
"""
|
|
137
|
+
super().__init__(**kwargs)
|
|
138
|
+
|
|
139
|
+
self.min_length = min_length
|
|
140
|
+
self.max_length = max_length
|
|
141
|
+
self.pattern = pattern
|
|
142
|
+
|
|
143
|
+
# Add length validators
|
|
144
|
+
if min_length is not None:
|
|
145
|
+
self.validators.append(lambda x: self._validate_min_length(x))
|
|
146
|
+
if max_length is not None:
|
|
147
|
+
self.validators.append(lambda x: self._validate_max_length(x))
|
|
148
|
+
if pattern is not None:
|
|
149
|
+
self.validators.append(lambda x: self._validate_pattern(x))
|
|
150
|
+
|
|
151
|
+
def _validate_min_length(self, value: str) -> bool:
|
|
152
|
+
if len(value) < self.min_length:
|
|
153
|
+
raise ValidationError(f"String too short, minimum length is {self.min_length}")
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
def _validate_max_length(self, value: str) -> bool:
|
|
157
|
+
if len(value) > self.max_length:
|
|
158
|
+
raise ValidationError(f"String too long, maximum length is {self.max_length}")
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
def _validate_pattern(self, value: str) -> bool:
|
|
162
|
+
if not re.match(self.pattern, value):
|
|
163
|
+
raise ValidationError(f"String does not match pattern: {self.pattern}")
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
def convert_type(self, value: Any) -> str:
|
|
167
|
+
"""Convert value to string"""
|
|
168
|
+
if isinstance(value, str):
|
|
169
|
+
return value
|
|
170
|
+
return str(value)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class IntField(Field):
|
|
174
|
+
"""Integer field with range validation"""
|
|
175
|
+
|
|
176
|
+
def __init__(
|
|
177
|
+
self,
|
|
178
|
+
min_value: Optional[int] = None,
|
|
179
|
+
max_value: Optional[int] = None,
|
|
180
|
+
**kwargs
|
|
181
|
+
):
|
|
182
|
+
"""
|
|
183
|
+
Initialize integer field
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
min_value: Minimum value
|
|
187
|
+
max_value: Maximum value
|
|
188
|
+
**kwargs: Base field arguments
|
|
189
|
+
"""
|
|
190
|
+
super().__init__(**kwargs)
|
|
191
|
+
|
|
192
|
+
self.min_value = min_value
|
|
193
|
+
self.max_value = max_value
|
|
194
|
+
|
|
195
|
+
# Add range validators
|
|
196
|
+
if min_value is not None:
|
|
197
|
+
self.validators.append(lambda x: self._validate_min_value(x))
|
|
198
|
+
if max_value is not None:
|
|
199
|
+
self.validators.append(lambda x: self._validate_max_value(x))
|
|
200
|
+
|
|
201
|
+
def _validate_min_value(self, value: int) -> bool:
|
|
202
|
+
if value < self.min_value:
|
|
203
|
+
raise ValidationError(f"Value too small, minimum is {self.min_value}")
|
|
204
|
+
return True
|
|
205
|
+
|
|
206
|
+
def _validate_max_value(self, value: int) -> bool:
|
|
207
|
+
if value > self.max_value:
|
|
208
|
+
raise ValidationError(f"Value too large, maximum is {self.max_value}")
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
def convert_type(self, value: Any) -> int:
|
|
212
|
+
"""Convert value to integer"""
|
|
213
|
+
if isinstance(value, int):
|
|
214
|
+
return value
|
|
215
|
+
if isinstance(value, float) and value.is_integer():
|
|
216
|
+
return int(value)
|
|
217
|
+
if isinstance(value, str):
|
|
218
|
+
try:
|
|
219
|
+
return int(value)
|
|
220
|
+
except ValueError:
|
|
221
|
+
raise ValueError("Cannot convert to integer")
|
|
222
|
+
raise ValueError("Cannot convert to integer")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class FloatField(Field):
|
|
226
|
+
"""Float field with range validation"""
|
|
227
|
+
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
min_value: Optional[float] = None,
|
|
231
|
+
max_value: Optional[float] = None,
|
|
232
|
+
**kwargs
|
|
233
|
+
):
|
|
234
|
+
"""
|
|
235
|
+
Initialize float field
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
min_value: Minimum value
|
|
239
|
+
max_value: Maximum value
|
|
240
|
+
**kwargs: Base field arguments
|
|
241
|
+
"""
|
|
242
|
+
super().__init__(**kwargs)
|
|
243
|
+
|
|
244
|
+
self.min_value = min_value
|
|
245
|
+
self.max_value = max_value
|
|
246
|
+
|
|
247
|
+
# Add range validators
|
|
248
|
+
if min_value is not None:
|
|
249
|
+
self.validators.append(lambda x: self._validate_min_value(x))
|
|
250
|
+
if max_value is not None:
|
|
251
|
+
self.validators.append(lambda x: self._validate_max_value(x))
|
|
252
|
+
|
|
253
|
+
def _validate_min_value(self, value: float) -> bool:
|
|
254
|
+
if value < self.min_value:
|
|
255
|
+
raise ValidationError(f"Value too small, minimum is {self.min_value}")
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
def _validate_max_value(self, value: float) -> bool:
|
|
259
|
+
if value > self.max_value:
|
|
260
|
+
raise ValidationError(f"Value too large, maximum is {self.max_value}")
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
def convert_type(self, value: Any) -> float:
|
|
264
|
+
"""Convert value to float"""
|
|
265
|
+
if isinstance(value, (int, float)):
|
|
266
|
+
return float(value)
|
|
267
|
+
if isinstance(value, str):
|
|
268
|
+
try:
|
|
269
|
+
return float(value)
|
|
270
|
+
except ValueError:
|
|
271
|
+
raise ValueError("Cannot convert to float")
|
|
272
|
+
raise ValueError("Cannot convert to float")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class BoolField(Field):
|
|
276
|
+
"""Boolean field"""
|
|
277
|
+
|
|
278
|
+
def convert_type(self, value: Any) -> bool:
|
|
279
|
+
"""Convert value to boolean"""
|
|
280
|
+
if isinstance(value, bool):
|
|
281
|
+
return value
|
|
282
|
+
if isinstance(value, str):
|
|
283
|
+
if value.lower() in ('true', '1', 'yes', 'on'):
|
|
284
|
+
return True
|
|
285
|
+
elif value.lower() in ('false', '0', 'no', 'off'):
|
|
286
|
+
return False
|
|
287
|
+
else:
|
|
288
|
+
raise ValueError("Cannot convert string to boolean")
|
|
289
|
+
if isinstance(value, (int, float)):
|
|
290
|
+
return bool(value)
|
|
291
|
+
raise ValueError("Cannot convert to boolean")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class EmailField(StringField):
|
|
295
|
+
"""Email field with email validation"""
|
|
296
|
+
|
|
297
|
+
def __init__(self, **kwargs):
|
|
298
|
+
"""Initialize email field"""
|
|
299
|
+
super().__init__(**kwargs)
|
|
300
|
+
self.validators.append(self._validate_email)
|
|
301
|
+
|
|
302
|
+
def _validate_email(self, value: str) -> bool:
|
|
303
|
+
"""Validate email format"""
|
|
304
|
+
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
305
|
+
if not re.match(email_pattern, value):
|
|
306
|
+
raise ValidationError("Invalid email format")
|
|
307
|
+
return True
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class URLField(StringField):
|
|
311
|
+
"""URL field with URL validation"""
|
|
312
|
+
|
|
313
|
+
def __init__(self, **kwargs):
|
|
314
|
+
"""Initialize URL field"""
|
|
315
|
+
super().__init__(**kwargs)
|
|
316
|
+
self.validators.append(self._validate_url)
|
|
317
|
+
|
|
318
|
+
def _validate_url(self, value: str) -> bool:
|
|
319
|
+
"""Validate URL format"""
|
|
320
|
+
url_pattern = r'^https?://(?:[-\w.])+(?:[:\d]+)?(?:/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:\w*))?)?$'
|
|
321
|
+
if not re.match(url_pattern, value):
|
|
322
|
+
raise ValidationError("Invalid URL format")
|
|
323
|
+
return True
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class DateField(Field):
|
|
327
|
+
"""Date field with date parsing"""
|
|
328
|
+
|
|
329
|
+
def __init__(self, format: str = "%Y-%m-%d", **kwargs):
|
|
330
|
+
"""
|
|
331
|
+
Initialize date field
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
format: Date format string
|
|
335
|
+
**kwargs: Base field arguments
|
|
336
|
+
"""
|
|
337
|
+
super().__init__(**kwargs)
|
|
338
|
+
self.format = format
|
|
339
|
+
|
|
340
|
+
def convert_type(self, value: Any) -> date:
|
|
341
|
+
"""Convert value to date"""
|
|
342
|
+
if isinstance(value, date):
|
|
343
|
+
return value
|
|
344
|
+
if isinstance(value, datetime):
|
|
345
|
+
return value.date()
|
|
346
|
+
if isinstance(value, str):
|
|
347
|
+
try:
|
|
348
|
+
return datetime.strptime(value, self.format).date()
|
|
349
|
+
except ValueError:
|
|
350
|
+
raise ValueError(f"Date does not match format {self.format}")
|
|
351
|
+
raise ValueError("Cannot convert to date")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class ListField(Field):
|
|
355
|
+
"""List field with item validation"""
|
|
356
|
+
|
|
357
|
+
def __init__(
|
|
358
|
+
self,
|
|
359
|
+
item_field: Optional[Field] = None,
|
|
360
|
+
min_items: Optional[int] = None,
|
|
361
|
+
max_items: Optional[int] = None,
|
|
362
|
+
**kwargs
|
|
363
|
+
):
|
|
364
|
+
"""
|
|
365
|
+
Initialize list field
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
item_field: Field for validating list items
|
|
369
|
+
min_items: Minimum number of items
|
|
370
|
+
max_items: Maximum number of items
|
|
371
|
+
**kwargs: Base field arguments
|
|
372
|
+
"""
|
|
373
|
+
super().__init__(**kwargs)
|
|
374
|
+
|
|
375
|
+
self.item_field = item_field
|
|
376
|
+
self.min_items = min_items
|
|
377
|
+
self.max_items = max_items
|
|
378
|
+
|
|
379
|
+
# Add list size validators
|
|
380
|
+
if min_items is not None:
|
|
381
|
+
self.validators.append(lambda x: self._validate_min_items(x))
|
|
382
|
+
if max_items is not None:
|
|
383
|
+
self.validators.append(lambda x: self._validate_max_items(x))
|
|
384
|
+
|
|
385
|
+
def _validate_min_items(self, value: list) -> bool:
|
|
386
|
+
if len(value) < self.min_items:
|
|
387
|
+
raise ValidationError(f"List too short, minimum items is {self.min_items}")
|
|
388
|
+
return True
|
|
389
|
+
|
|
390
|
+
def _validate_max_items(self, value: list) -> bool:
|
|
391
|
+
if len(value) > self.max_items:
|
|
392
|
+
raise ValidationError(f"List too long, maximum items is {self.max_items}")
|
|
393
|
+
return True
|
|
394
|
+
|
|
395
|
+
def convert_type(self, value: Any) -> list:
|
|
396
|
+
"""Convert value to list"""
|
|
397
|
+
if isinstance(value, list):
|
|
398
|
+
return value
|
|
399
|
+
if isinstance(value, (tuple, set)):
|
|
400
|
+
return list(value)
|
|
401
|
+
# Single item becomes list of one item
|
|
402
|
+
return [value]
|
|
403
|
+
|
|
404
|
+
def validate(self, value: Any, field_name: str = "field") -> list:
|
|
405
|
+
"""Validate list and its items"""
|
|
406
|
+
# Validate the list itself
|
|
407
|
+
validated_list = super().validate(value, field_name)
|
|
408
|
+
|
|
409
|
+
if validated_list is None:
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
# Validate each item if item_field is provided
|
|
413
|
+
if self.item_field:
|
|
414
|
+
validated_items = []
|
|
415
|
+
for i, item in enumerate(validated_list):
|
|
416
|
+
try:
|
|
417
|
+
validated_item = self.item_field.validate(item, f"{field_name}[{i}]")
|
|
418
|
+
validated_items.append(validated_item)
|
|
419
|
+
except ValidationError as e:
|
|
420
|
+
raise ValidationError(f"Item {i} in {field_name}: {e}")
|
|
421
|
+
return validated_items
|
|
422
|
+
|
|
423
|
+
return validated_list
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class DictField(Field):
|
|
427
|
+
"""Dictionary field with key/value validation"""
|
|
428
|
+
|
|
429
|
+
def __init__(
|
|
430
|
+
self,
|
|
431
|
+
key_field: Optional[Field] = None,
|
|
432
|
+
value_field: Optional[Field] = None,
|
|
433
|
+
**kwargs
|
|
434
|
+
):
|
|
435
|
+
"""
|
|
436
|
+
Initialize dictionary field
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
key_field: Field for validating dictionary keys
|
|
440
|
+
value_field: Field for validating dictionary values
|
|
441
|
+
**kwargs: Base field arguments
|
|
442
|
+
"""
|
|
443
|
+
super().__init__(**kwargs)
|
|
444
|
+
|
|
445
|
+
self.key_field = key_field
|
|
446
|
+
self.value_field = value_field
|
|
447
|
+
|
|
448
|
+
def convert_type(self, value: Any) -> dict:
|
|
449
|
+
"""Convert value to dictionary"""
|
|
450
|
+
if isinstance(value, dict):
|
|
451
|
+
return value
|
|
452
|
+
raise ValueError("Cannot convert to dictionary")
|
|
453
|
+
|
|
454
|
+
def validate(self, value: Any, field_name: str = "field") -> dict:
|
|
455
|
+
"""Validate dictionary and its keys/values"""
|
|
456
|
+
# Validate the dictionary itself
|
|
457
|
+
validated_dict = super().validate(value, field_name)
|
|
458
|
+
|
|
459
|
+
if validated_dict is None:
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
# Validate keys and values if field validators are provided
|
|
463
|
+
if self.key_field or self.value_field:
|
|
464
|
+
validated_items = {}
|
|
465
|
+
for key, val in validated_dict.items():
|
|
466
|
+
# Validate key
|
|
467
|
+
if self.key_field:
|
|
468
|
+
try:
|
|
469
|
+
validated_key = self.key_field.validate(key, f"{field_name} key")
|
|
470
|
+
except ValidationError as e:
|
|
471
|
+
raise ValidationError(f"Key '{key}' in {field_name}: {e}")
|
|
472
|
+
else:
|
|
473
|
+
validated_key = key
|
|
474
|
+
|
|
475
|
+
# Validate value
|
|
476
|
+
if self.value_field:
|
|
477
|
+
try:
|
|
478
|
+
validated_value = self.value_field.validate(val, f"{field_name}[{key}]")
|
|
479
|
+
except ValidationError as e:
|
|
480
|
+
raise ValidationError(f"Value for key '{key}' in {field_name}: {e}")
|
|
481
|
+
else:
|
|
482
|
+
validated_value = val
|
|
483
|
+
|
|
484
|
+
validated_items[validated_key] = validated_value
|
|
485
|
+
|
|
486
|
+
return validated_items
|
|
487
|
+
|
|
488
|
+
return validated_dict
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class OptionalField(Field):
|
|
492
|
+
"""Optional field wrapper"""
|
|
493
|
+
|
|
494
|
+
def __init__(self, inner_field: Field, **kwargs):
|
|
495
|
+
"""
|
|
496
|
+
Initialize optional field
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
inner_field: The field to make optional
|
|
500
|
+
**kwargs: Base field arguments
|
|
501
|
+
"""
|
|
502
|
+
# Override required to False
|
|
503
|
+
kwargs['required'] = False
|
|
504
|
+
super().__init__(**kwargs)
|
|
505
|
+
|
|
506
|
+
self.inner_field = inner_field
|
|
507
|
+
# Copy inner field properties but make it non-required
|
|
508
|
+
self.inner_field.required = False
|
|
509
|
+
|
|
510
|
+
def validate(self, value: Any, field_name: str = "field") -> Any:
|
|
511
|
+
"""Validate optional field"""
|
|
512
|
+
if value is None:
|
|
513
|
+
return self.default
|
|
514
|
+
|
|
515
|
+
return self.inner_field.validate(value, field_name)
|
|
516
|
+
|
|
517
|
+
def convert_type(self, value: Any) -> Any:
|
|
518
|
+
"""Use inner field's type conversion"""
|
|
519
|
+
return self.inner_field.convert_type(value)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class ChoiceField(Field):
|
|
523
|
+
"""Field that validates against a set of choices"""
|
|
524
|
+
|
|
525
|
+
def __init__(self, choices: List[Any], **kwargs):
|
|
526
|
+
"""
|
|
527
|
+
Initialize choice field
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
choices: List of valid choices
|
|
531
|
+
**kwargs: Base field arguments
|
|
532
|
+
"""
|
|
533
|
+
super().__init__(**kwargs)
|
|
534
|
+
|
|
535
|
+
self.choices = choices
|
|
536
|
+
self.validators.append(self._validate_choice)
|
|
537
|
+
|
|
538
|
+
def _validate_choice(self, value: Any) -> bool:
|
|
539
|
+
if value not in self.choices:
|
|
540
|
+
raise ValidationError(f"Value must be one of: {self.choices}")
|
|
541
|
+
return True
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
# Utility function for creating optional fields
|
|
545
|
+
def Optional(field: Field, default: Any = None) -> OptionalField:
|
|
546
|
+
"""Create an optional version of a field"""
|
|
547
|
+
return OptionalField(field, default=default)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# Common field factory functions
|
|
551
|
+
def String(min_length=None, max_length=None, pattern=None, **kwargs):
|
|
552
|
+
"""Create a string field"""
|
|
553
|
+
return StringField(min_length=min_length, max_length=max_length, pattern=pattern, **kwargs)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def Int(min_value=None, max_value=None, **kwargs):
|
|
557
|
+
"""Create an integer field"""
|
|
558
|
+
return IntField(min_value=min_value, max_value=max_value, **kwargs)
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def Float(min_value=None, max_value=None, **kwargs):
|
|
562
|
+
"""Create a float field"""
|
|
563
|
+
return FloatField(min_value=min_value, max_value=max_value, **kwargs)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def Bool(**kwargs) -> BoolField:
|
|
567
|
+
"""Create a boolean field"""
|
|
568
|
+
return BoolField(**kwargs)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def Email(**kwargs) -> EmailField:
|
|
572
|
+
"""Create an email field"""
|
|
573
|
+
return EmailField(**kwargs)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def URL(**kwargs) -> URLField:
|
|
577
|
+
"""Create a URL field"""
|
|
578
|
+
return URLField(**kwargs)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def Date(format: str = "%Y-%m-%d", **kwargs) -> DateField:
|
|
582
|
+
"""Create a date field"""
|
|
583
|
+
return DateField(format=format, **kwargs)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def List(item_field=None, min_items=None, max_items=None, **kwargs):
|
|
587
|
+
"""Create a list field"""
|
|
588
|
+
return ListField(item_field=item_field, min_items=min_items, max_items=max_items, **kwargs)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def Dict(key_field=None, value_field=None, **kwargs):
|
|
592
|
+
"""Create a dictionary field"""
|
|
593
|
+
return DictField(key_field=key_field, value_field=value_field, **kwargs)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def Choice(choices, **kwargs):
|
|
597
|
+
"""Create a choice field"""
|
|
598
|
+
return ChoiceField(choices=choices, **kwargs)
|