Coverage for src/dataknobs_data/validation_old_backup/constraints.py: 0%
179 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-15 12:32 -0500
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-15 12:32 -0500
1"""Field constraints for schema validation."""
3import re
4from abc import ABC, abstractmethod
5from typing import Any, Callable, Dict, List, Optional, Set, Union
8class Constraint(ABC):
9 """Base class for field constraints."""
11 def __init__(self, name: str = ""):
12 """Initialize constraint.
14 Args:
15 name: Constraint name for error messages
16 """
17 self.name = name or self.__class__.__name__
19 @abstractmethod
20 def validate(self, value: Any) -> bool:
21 """Validate a value against this constraint.
23 Args:
24 value: Value to validate
26 Returns:
27 True if valid, False otherwise
28 """
29 pass
31 @abstractmethod
32 def get_error_message(self, value: Any) -> str:
33 """Get error message for failed validation.
35 Args:
36 value: The invalid value
38 Returns:
39 Error message string
40 """
41 pass
43 def to_dict(self) -> Dict[str, Any]:
44 """Convert constraint to dictionary."""
45 return {
46 'type': self.__class__.__name__,
47 'name': self.name
48 }
50 @classmethod
51 def from_dict(cls, data: Dict[str, Any]) -> "Constraint":
52 """Create constraint from dictionary."""
53 constraint_type = data.get('type', 'Constraint')
55 # Map type names to classes
56 type_map = {
57 'RequiredConstraint': RequiredConstraint,
58 'UniqueConstraint': UniqueConstraint,
59 'MinValueConstraint': MinValueConstraint,
60 'MaxValueConstraint': MaxValueConstraint,
61 'MinLengthConstraint': MinLengthConstraint,
62 'MaxLengthConstraint': MaxLengthConstraint,
63 'PatternConstraint': PatternConstraint,
64 'EnumConstraint': EnumConstraint,
65 'CustomConstraint': CustomConstraint,
66 }
68 constraint_class = type_map.get(constraint_type, cls)
69 return constraint_class(**data)
72class RequiredConstraint(Constraint):
73 """Constraint that requires a non-null value."""
75 def __init__(self, allow_empty: bool = False, **kwargs):
76 """Initialize required constraint.
78 Args:
79 allow_empty: Whether to allow empty strings/collections
80 """
81 super().__init__(**kwargs)
82 self.allow_empty = allow_empty
84 def validate(self, value: Any) -> bool:
85 """Check if value is not None."""
86 if value is None:
87 return False
89 if not self.allow_empty:
90 if isinstance(value, (str, list, dict)) and len(value) == 0:
91 return False
93 return True
95 def get_error_message(self, value: Any) -> str:
96 """Get error message."""
97 if value is None:
98 return "Value is required"
99 return "Value cannot be empty"
101 def to_dict(self) -> Dict[str, Any]:
102 """Convert to dictionary."""
103 data = super().to_dict()
104 data['allow_empty'] = self.allow_empty
105 return data
108class UniqueConstraint(Constraint):
109 """Constraint that requires unique values."""
111 def __init__(self, scope: str = "global", **kwargs):
112 """Initialize unique constraint.
114 Args:
115 scope: Uniqueness scope ('global', 'collection', etc.)
116 """
117 super().__init__(**kwargs)
118 self.scope = scope
119 self.seen_values: Set[Any] = set()
121 def validate(self, value: Any) -> bool:
122 """Check if value is unique."""
123 if value in self.seen_values:
124 return False
125 self.seen_values.add(value)
126 return True
128 def get_error_message(self, value: Any) -> str:
129 """Get error message."""
130 return f"Value must be unique (duplicate: {value})"
132 def reset(self) -> None:
133 """Reset seen values."""
134 self.seen_values.clear()
136 def to_dict(self) -> Dict[str, Any]:
137 """Convert to dictionary."""
138 data = super().to_dict()
139 data['scope'] = self.scope
140 return data
143class MinValueConstraint(Constraint):
144 """Constraint for minimum numeric value."""
146 def __init__(self, min_value: Union[int, float], inclusive: bool = True, **kwargs):
147 """Initialize minimum value constraint.
149 Args:
150 min_value: Minimum allowed value
151 inclusive: Whether the minimum is inclusive
152 """
153 super().__init__(**kwargs)
154 self.min_value = min_value
155 self.inclusive = inclusive
157 def validate(self, value: Any) -> bool:
158 """Check if value meets minimum."""
159 try:
160 numeric_value = float(value)
161 if self.inclusive:
162 return numeric_value >= self.min_value
163 return numeric_value > self.min_value
164 except (TypeError, ValueError):
165 return False
167 def get_error_message(self, value: Any) -> str:
168 """Get error message."""
169 op = ">=" if self.inclusive else ">"
170 return f"Value must be {op} {self.min_value} (got {value})"
172 def to_dict(self) -> Dict[str, Any]:
173 """Convert to dictionary."""
174 data = super().to_dict()
175 data['min_value'] = self.min_value
176 data['inclusive'] = self.inclusive
177 return data
180class MaxValueConstraint(Constraint):
181 """Constraint for maximum numeric value."""
183 def __init__(self, max_value: Union[int, float], inclusive: bool = True, **kwargs):
184 """Initialize maximum value constraint.
186 Args:
187 max_value: Maximum allowed value
188 inclusive: Whether the maximum is inclusive
189 """
190 super().__init__(**kwargs)
191 self.max_value = max_value
192 self.inclusive = inclusive
194 def validate(self, value: Any) -> bool:
195 """Check if value meets maximum."""
196 try:
197 numeric_value = float(value)
198 if self.inclusive:
199 return numeric_value <= self.max_value
200 return numeric_value < self.max_value
201 except (TypeError, ValueError):
202 return False
204 def get_error_message(self, value: Any) -> str:
205 """Get error message."""
206 op = "<=" if self.inclusive else "<"
207 return f"Value must be {op} {self.max_value} (got {value})"
209 def to_dict(self) -> Dict[str, Any]:
210 """Convert to dictionary."""
211 data = super().to_dict()
212 data['max_value'] = self.max_value
213 data['inclusive'] = self.inclusive
214 return data
217class MinLengthConstraint(Constraint):
218 """Constraint for minimum string/collection length."""
220 def __init__(self, min_length: int, **kwargs):
221 """Initialize minimum length constraint.
223 Args:
224 min_length: Minimum required length
225 """
226 super().__init__(**kwargs)
227 self.min_length = min_length
229 def validate(self, value: Any) -> bool:
230 """Check if value meets minimum length."""
231 try:
232 return len(value) >= self.min_length
233 except TypeError:
234 return False
236 def get_error_message(self, value: Any) -> str:
237 """Get error message."""
238 try:
239 actual_length = len(value)
240 return f"Length must be at least {self.min_length} (got {actual_length})"
241 except TypeError:
242 return f"Value must have a length (got {type(value).__name__})"
244 def to_dict(self) -> Dict[str, Any]:
245 """Convert to dictionary."""
246 data = super().to_dict()
247 data['min_length'] = self.min_length
248 return data
251class MaxLengthConstraint(Constraint):
252 """Constraint for maximum string/collection length."""
254 def __init__(self, max_length: int, **kwargs):
255 """Initialize maximum length constraint.
257 Args:
258 max_length: Maximum allowed length
259 """
260 super().__init__(**kwargs)
261 self.max_length = max_length
263 def validate(self, value: Any) -> bool:
264 """Check if value meets maximum length."""
265 try:
266 return len(value) <= self.max_length
267 except TypeError:
268 return False
270 def get_error_message(self, value: Any) -> str:
271 """Get error message."""
272 try:
273 actual_length = len(value)
274 return f"Length must be at most {self.max_length} (got {actual_length})"
275 except TypeError:
276 return f"Value must have a length (got {type(value).__name__})"
278 def to_dict(self) -> Dict[str, Any]:
279 """Convert to dictionary."""
280 data = super().to_dict()
281 data['max_length'] = self.max_length
282 return data
285class PatternConstraint(Constraint):
286 """Constraint for regex pattern matching."""
288 def __init__(self, pattern: str, flags: int = 0, **kwargs):
289 """Initialize pattern constraint.
291 Args:
292 pattern: Regular expression pattern
293 flags: Regex flags (e.g., re.IGNORECASE)
294 """
295 super().__init__(**kwargs)
296 self.pattern = pattern
297 self.flags = flags
298 self._regex = re.compile(pattern, flags)
300 def validate(self, value: Any) -> bool:
301 """Check if value matches pattern."""
302 try:
303 string_value = str(value)
304 return self._regex.match(string_value) is not None
305 except Exception:
306 return False
308 def get_error_message(self, value: Any) -> str:
309 """Get error message."""
310 return f"Value must match pattern: {self.pattern} (got {value})"
312 def to_dict(self) -> Dict[str, Any]:
313 """Convert to dictionary."""
314 data = super().to_dict()
315 data['pattern'] = self.pattern
316 data['flags'] = self.flags
317 return data
320class EnumConstraint(Constraint):
321 """Constraint for enumerated values."""
323 def __init__(self, allowed_values: List[Any], **kwargs):
324 """Initialize enum constraint.
326 Args:
327 allowed_values: List of allowed values
328 """
329 super().__init__(**kwargs)
330 self.allowed_values = set(allowed_values)
332 def validate(self, value: Any) -> bool:
333 """Check if value is in allowed set."""
334 return value in self.allowed_values
336 def get_error_message(self, value: Any) -> str:
337 """Get error message."""
338 allowed_str = ", ".join(str(v) for v in sorted(self.allowed_values))
339 return f"Value must be one of: {allowed_str} (got {value})"
341 def to_dict(self) -> Dict[str, Any]:
342 """Convert to dictionary."""
343 data = super().to_dict()
344 data['allowed_values'] = list(self.allowed_values)
345 return data
348class CustomConstraint(Constraint):
349 """Constraint with custom validation function."""
351 def __init__(
352 self,
353 validator: Optional[Callable[[Any], bool]] = None,
354 error_message: str = "Custom validation failed",
355 **kwargs
356 ):
357 """Initialize custom constraint.
359 Args:
360 validator: Custom validation function
361 error_message: Error message for failures
362 """
363 super().__init__(**kwargs)
364 self.validator = validator or (lambda x: True)
365 self.error_message = error_message
367 def validate(self, value: Any) -> bool:
368 """Apply custom validation."""
369 try:
370 return self.validator(value)
371 except Exception:
372 return False
374 def get_error_message(self, value: Any) -> str:
375 """Get error message."""
376 return self.error_message.format(value=value) if "{value}" in self.error_message else self.error_message
378 def to_dict(self) -> Dict[str, Any]:
379 """Convert to dictionary."""
380 data = super().to_dict()
381 data['error_message'] = self.error_message
382 # Note: Cannot serialize arbitrary functions
383 return data