string-schema 0.1.0__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.
@@ -0,0 +1,501 @@
1
+ """
2
+ Pydantic Utility Functions for Simple Schema
3
+
4
+ This module provides the core utility functions that transform Simple Schema
5
+ into a comprehensive Pydantic utility, enabling string-based schema usage
6
+ throughout the Python ecosystem.
7
+
8
+ Key Functions:
9
+ - create_model(): String schema → Pydantic model
10
+ - validate_to_dict(): Validate data → dict
11
+ - validate_to_model(): Validate data → Pydantic model
12
+ - returns_dict(): Decorator for dict validation
13
+ - returns_model(): Decorator for model validation
14
+ """
15
+
16
+ import functools
17
+ import uuid
18
+ from typing import Any, Dict, Type, Union, Callable, Optional, List
19
+ import logging
20
+
21
+ # Optional pydantic import
22
+ try:
23
+ from pydantic import BaseModel, ValidationError
24
+ HAS_PYDANTIC = True
25
+ except ImportError:
26
+ HAS_PYDANTIC = False
27
+ BaseModel = None
28
+ ValidationError = None
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def string_to_model(schema_str: str, name: Optional[str] = None) -> Type[BaseModel]:
34
+ """
35
+ Create Pydantic model from string schema.
36
+
37
+ This is the main utility function that converts string schemas directly
38
+ to Pydantic model classes, eliminating verbose model definitions.
39
+
40
+ Args:
41
+ schema_str: String schema definition (e.g., "name:string, email:email, age:int?")
42
+ name: Optional name for the model class. If not provided, generates one.
43
+
44
+ Returns:
45
+ Dynamically created Pydantic model class
46
+
47
+ Examples:
48
+ # Basic usage
49
+ UserModel = string_to_model("name:string(min=1,max=100), email:email, age:int(0,120)?")
50
+ user = UserModel(name="John", email="john@example.com", age=30)
51
+
52
+ # Array schemas
53
+ ProductModel = string_to_model("[{name:string, price:number(min=0), category:enum(electronics,clothing,books)}]")
54
+ products = ProductModel([{"name": "iPhone", "price": 999, "category": "electronics"}])
55
+
56
+ # Complex nested schemas
57
+ ProfileModel = string_to_model("name:string, email:email, profile:{bio:text?, avatar:url?}?")
58
+ """
59
+ if not HAS_PYDANTIC:
60
+ raise ImportError("Pydantic is required for string_to_model. Install with: pip install pydantic")
61
+
62
+ # Import here to avoid circular imports
63
+ from .parsing.string_parser import parse_string_schema
64
+ from pydantic import create_model as pydantic_create_model, Field
65
+ from typing import List
66
+
67
+ # Generate model name if not provided
68
+ if name is None:
69
+ name = f"GeneratedModel_{str(uuid.uuid4()).replace('-', '')[:8]}"
70
+
71
+ try:
72
+ # Validate the schema string first
73
+ from .parsing.string_parser import validate_string_schema
74
+ validation_result = validate_string_schema(schema_str)
75
+ if not validation_result['valid']:
76
+ error_msg = "Invalid schema syntax"
77
+ if validation_result['errors']:
78
+ error_msg += f": {', '.join(validation_result['errors'])}"
79
+ raise ValueError(error_msg)
80
+
81
+ # Convert string to JSON Schema
82
+ json_schema = parse_string_schema(schema_str)
83
+
84
+ # Handle array schemas specially
85
+ if json_schema.get('type') == 'array':
86
+ # For array schemas, use RootModel for Pydantic v2 compatibility
87
+ items_schema = json_schema.get('items', {})
88
+
89
+ try:
90
+ # Try Pydantic v2 RootModel first
91
+ from pydantic import RootModel
92
+
93
+ if items_schema.get('type') == 'object':
94
+ # Array of objects: create nested model for items
95
+ from .integrations.pydantic import create_pydantic_from_json_schema
96
+ ItemModel = create_pydantic_from_json_schema(f"{name}Item", items_schema)
97
+
98
+ # Create the array model using RootModel
99
+ class ArrayModel(RootModel[List[ItemModel]]):
100
+ pass
101
+
102
+ # Set the name
103
+ ArrayModel.__name__ = name
104
+ return ArrayModel
105
+ else:
106
+ # Array of simple types
107
+ type_mapping = {
108
+ 'string': str,
109
+ 'integer': int,
110
+ 'number': float,
111
+ 'boolean': bool
112
+ }
113
+ item_type = type_mapping.get(items_schema.get('type', 'string'), str)
114
+
115
+ # Create the array model using RootModel
116
+ class ArrayModel(RootModel[List[item_type]]):
117
+ pass
118
+
119
+ # Set the name
120
+ ArrayModel.__name__ = name
121
+ return ArrayModel
122
+
123
+ except ImportError:
124
+ # Fallback to Pydantic v1 style with __root__
125
+ if items_schema.get('type') == 'object':
126
+ # Array of objects: create nested model for items
127
+ from .integrations.pydantic import create_pydantic_from_json_schema
128
+ ItemModel = create_pydantic_from_json_schema(f"{name}Item", items_schema)
129
+
130
+ # Create constraints for the array
131
+ constraints = {}
132
+ if 'minItems' in json_schema:
133
+ constraints['min_length'] = json_schema['minItems']
134
+ if 'maxItems' in json_schema:
135
+ constraints['max_length'] = json_schema['maxItems']
136
+
137
+ # Create the array model
138
+ field_info = Field(**constraints) if constraints else Field()
139
+ return pydantic_create_model(name, __root__=(List[ItemModel], field_info))
140
+ else:
141
+ # Array of simple types
142
+ type_mapping = {
143
+ 'string': str,
144
+ 'integer': int,
145
+ 'number': float,
146
+ 'boolean': bool
147
+ }
148
+ item_type = type_mapping.get(items_schema.get('type', 'string'), str)
149
+
150
+ # Create constraints for the array
151
+ constraints = {}
152
+ if 'minItems' in json_schema:
153
+ constraints['min_length'] = json_schema['minItems']
154
+ if 'maxItems' in json_schema:
155
+ constraints['max_length'] = json_schema['maxItems']
156
+
157
+ field_info = Field(**constraints) if constraints else Field()
158
+ return pydantic_create_model(name, __root__=(List[item_type], field_info))
159
+ else:
160
+ # Regular object schema
161
+ from .integrations.pydantic import create_pydantic_from_json_schema
162
+ return create_pydantic_from_json_schema(name, json_schema)
163
+
164
+ except Exception as e:
165
+ raise ValueError(f"Failed to create model from schema '{schema_str}': {str(e)}") from e
166
+
167
+
168
+ # Legacy alias for backward compatibility
169
+ def create_model(schema_str: str, name: Optional[str] = None) -> Type[BaseModel]:
170
+ """
171
+ Create Pydantic model from string schema.
172
+
173
+ DEPRECATED: Use string_to_model() instead for consistent naming.
174
+ """
175
+ return string_to_model(schema_str, name)
176
+
177
+
178
+ def validate_to_dict(data: Union[Dict[str, Any], Any], schema_str: str) -> Union[Dict[str, Any], List[Any]]:
179
+ """
180
+ Validate data against string schema and return validated dict or list.
181
+
182
+ Perfect for API endpoints and JSON responses where you need validated
183
+ dictionaries rather than model instances.
184
+
185
+ Args:
186
+ data: Data to validate (dict, list, model instance, or any object)
187
+ schema_str: String schema definition for validation
188
+
189
+ Returns:
190
+ Validated dictionary or list
191
+
192
+ Raises:
193
+ ValidationError: If data doesn't match schema
194
+ ValueError: If schema is invalid
195
+
196
+ Examples:
197
+ # API endpoint usage
198
+ user_dict = validate_to_dict(raw_data, "name:string, email:email, age:int?")
199
+ # Returns: {"name": "John", "email": "john@example.com", "age": 30}
200
+
201
+ # Array validation
202
+ events = validate_to_dict(raw_events, "[{user_id:uuid, event:enum(login,logout,purchase), timestamp:datetime}]")
203
+ """
204
+ if not HAS_PYDANTIC:
205
+ raise ImportError("Pydantic is required for validate_to_dict. Install with: pip install pydantic")
206
+
207
+ try:
208
+ # Create temporary model for validation
209
+ TempModel = string_to_model(schema_str, "TempValidationModel")
210
+
211
+ # Check if this is an array schema
212
+ from .parsing.string_parser import parse_string_schema
213
+ json_schema = parse_string_schema(schema_str)
214
+ is_array_schema = json_schema.get('type') == 'array'
215
+
216
+ if is_array_schema:
217
+ # For array schemas, validate the data directly
218
+ try:
219
+ # Try Pydantic v2 RootModel style
220
+ validated_instance = TempModel(data)
221
+ # Return the validated array data
222
+ return validated_instance.model_dump() if hasattr(validated_instance, 'model_dump') else validated_instance.dict()
223
+ except:
224
+ # Fallback to Pydantic v1 style
225
+ validated_instance = TempModel(__root__=data)
226
+ # Return the validated array data
227
+ return validated_instance.model_dump()['__root__'] if hasattr(validated_instance, 'model_dump') else validated_instance.dict()['__root__']
228
+ else:
229
+ # Handle different input types for object schemas
230
+ if isinstance(data, dict):
231
+ validated_instance = TempModel(**data)
232
+ elif hasattr(data, '__dict__'):
233
+ # Handle objects with attributes
234
+ validated_instance = TempModel(**data.__dict__)
235
+ else:
236
+ # Try direct validation
237
+ validated_instance = TempModel(data)
238
+
239
+ # Return as dictionary (use model_dump for Pydantic v2, fallback to dict for v1)
240
+ if hasattr(validated_instance, 'model_dump'):
241
+ return validated_instance.model_dump()
242
+ else:
243
+ return validated_instance.dict()
244
+
245
+ except ValidationError as e:
246
+ # Re-raise the original validation error
247
+ raise e
248
+ except Exception as e:
249
+ raise ValueError(f"Failed to validate data against schema '{schema_str}': {str(e)}") from e
250
+
251
+
252
+ def validate_to_model(data: Union[Dict[str, Any], Any], schema_str: str):
253
+ """
254
+ Validate data against string schema and return Pydantic model instance.
255
+
256
+ Perfect for business logic where you need full Pydantic model features
257
+ like methods, validators, and type safety.
258
+
259
+ Args:
260
+ data: Data to validate (dict, list, model instance, or any object)
261
+ schema_str: String schema definition for validation
262
+
263
+ Returns:
264
+ Validated Pydantic model instance
265
+
266
+ Raises:
267
+ ValidationError: If data doesn't match schema
268
+ ValueError: If schema is invalid
269
+
270
+ Examples:
271
+ # Business logic usage
272
+ user_model = validate_to_model(raw_data, "name:string, email:email, age:int?")
273
+ print(user_model.name) # Access with full type safety
274
+
275
+ # Complex validation
276
+ profile = validate_to_model(data, "name:string, email:email, profile:{bio:text?, avatar:url?}?")
277
+ if profile.profile:
278
+ print(profile.profile.bio)
279
+ """
280
+ if not HAS_PYDANTIC:
281
+ raise ImportError("Pydantic is required for validate_to_model. Install with: pip install pydantic")
282
+
283
+ try:
284
+ # Create temporary model for validation
285
+ TempModel = string_to_model(schema_str, "TempValidationModel")
286
+
287
+ # Check if this is an array schema
288
+ from .parsing.string_parser import parse_string_schema
289
+ json_schema = parse_string_schema(schema_str)
290
+ is_array_schema = json_schema.get('type') == 'array'
291
+
292
+ if is_array_schema:
293
+ # For array schemas, validate the data directly
294
+ try:
295
+ # Try Pydantic v2 RootModel style
296
+ return TempModel(data)
297
+ except:
298
+ # Fallback to Pydantic v1 style
299
+ return TempModel(__root__=data)
300
+ else:
301
+ # Handle different input types for object schemas
302
+ if isinstance(data, dict):
303
+ return TempModel(**data)
304
+ elif hasattr(data, '__dict__'):
305
+ # Handle objects with attributes
306
+ return TempModel(**data.__dict__)
307
+ else:
308
+ # Try direct validation
309
+ return TempModel(data)
310
+
311
+ except ValidationError as e:
312
+ # Re-raise the original validation error
313
+ raise e
314
+ except Exception as e:
315
+ raise ValueError(f"Failed to validate data against schema '{schema_str}': {str(e)}") from e
316
+
317
+
318
+ def returns_dict(schema_str: str) -> Callable:
319
+ """
320
+ Decorator that validates function return values to dict format.
321
+
322
+ Perfect for API endpoints where you want to ensure consistent,
323
+ validated dictionary responses.
324
+
325
+ Args:
326
+ schema_str: String schema definition for return value validation
327
+
328
+ Returns:
329
+ Decorator function
330
+
331
+ Examples:
332
+ # API endpoint decorator
333
+ @returns_dict("id:uuid, name:string, status:enum(created,updated)")
334
+ def create_user(user_data):
335
+ # Business logic here
336
+ return {"id": generate_uuid(), "name": user_data["name"], "status": "created"}
337
+
338
+ # Data pipeline decorator
339
+ @returns_dict("[{user_id:uuid, event:enum(login,logout,purchase), timestamp:datetime}]")
340
+ def process_event_stream(raw_events):
341
+ # Transform and validate events
342
+ return transformed_events # Returns list of validated dicts
343
+ """
344
+ def decorator(func: Callable) -> Callable:
345
+ @functools.wraps(func)
346
+ def wrapper(*args, **kwargs):
347
+ result = func(*args, **kwargs)
348
+ try:
349
+ return validate_to_dict(result, schema_str)
350
+ except Exception as e:
351
+ raise ValueError(f"Function '{func.__name__}' returned invalid data for schema '{schema_str}': {str(e)}") from e
352
+ return wrapper
353
+ return decorator
354
+
355
+
356
+ def returns_model(schema_str: str) -> Callable:
357
+ """
358
+ Decorator that validates function return values to Pydantic model instances.
359
+
360
+ Perfect for business logic functions where you want to ensure type-safe
361
+ model instances with full Pydantic features.
362
+
363
+ Args:
364
+ schema_str: String schema definition for return value validation
365
+
366
+ Returns:
367
+ Decorator function
368
+
369
+ Examples:
370
+ # Business logic decorator
371
+ @returns_model("name:string, email:email, profile:{bio:text?, avatar:url?}?")
372
+ def enrich_user_data(basic_data):
373
+ # Enhancement logic here
374
+ return enhanced_user_data # Returns validated Pydantic model
375
+
376
+ # Data processing decorator
377
+ @returns_model("id:uuid, processed_at:datetime, result:{score:float(0,1), confidence:float(0,1)}")
378
+ def process_ml_result(raw_result):
379
+ # ML processing logic
380
+ return processed_result # Returns validated model with type safety
381
+ """
382
+ def decorator(func: Callable) -> Callable:
383
+ @functools.wraps(func)
384
+ def wrapper(*args, **kwargs):
385
+ result = func(*args, **kwargs)
386
+ try:
387
+ return validate_to_model(result, schema_str)
388
+ except Exception as e:
389
+ raise ValueError(f"Function '{func.__name__}' returned invalid data for schema '{schema_str}': {str(e)}") from e
390
+ return wrapper
391
+ return decorator
392
+
393
+
394
+ # Utility functions for enhanced error handling and debugging
395
+
396
+ def get_model_info(model_class) -> Dict[str, Any]:
397
+ """
398
+ Get detailed information about a generated Pydantic model.
399
+
400
+ Args:
401
+ model_class: Pydantic model class
402
+
403
+ Returns:
404
+ Dictionary with model information including fields, types, and constraints
405
+ """
406
+ if not HAS_PYDANTIC or not issubclass(model_class, BaseModel):
407
+ raise ValueError("Input must be a Pydantic model class")
408
+
409
+ info = {
410
+ "model_name": model_class.__name__,
411
+ "fields": {},
412
+ "required_fields": [],
413
+ "optional_fields": []
414
+ }
415
+
416
+ # Handle both Pydantic v1 and v2
417
+ if hasattr(model_class, 'model_fields'):
418
+ # Pydantic v2
419
+ fields_dict = model_class.model_fields
420
+ for field_name, field_info in fields_dict.items():
421
+ field_data = {
422
+ "type": str(field_info.annotation) if hasattr(field_info, 'annotation') else "unknown",
423
+ "required": field_info.is_required() if hasattr(field_info, 'is_required') else True,
424
+ "default": field_info.default if hasattr(field_info, 'default') and field_info.default is not ... else None,
425
+ "constraints": {}
426
+ }
427
+
428
+ # Extract constraints from field_info
429
+ if hasattr(field_info, 'constraints'):
430
+ constraints = field_info.constraints
431
+ for constraint in constraints:
432
+ if hasattr(constraint, 'ge') and constraint.ge is not None:
433
+ field_data["constraints"]["min_value"] = constraint.ge
434
+ if hasattr(constraint, 'le') and constraint.le is not None:
435
+ field_data["constraints"]["max_value"] = constraint.le
436
+ if hasattr(constraint, 'min_length') and constraint.min_length is not None:
437
+ field_data["constraints"]["min_length"] = constraint.min_length
438
+ if hasattr(constraint, 'max_length') and constraint.max_length is not None:
439
+ field_data["constraints"]["max_length"] = constraint.max_length
440
+
441
+ info["fields"][field_name] = field_data
442
+
443
+ if field_data["required"]:
444
+ info["required_fields"].append(field_name)
445
+ else:
446
+ info["optional_fields"].append(field_name)
447
+ else:
448
+ # Pydantic v1 fallback
449
+ fields_dict = getattr(model_class, '__fields__', {})
450
+ for field_name, field_info in fields_dict.items():
451
+ field_data = {
452
+ "type": str(getattr(field_info, 'type_', 'unknown')),
453
+ "required": getattr(field_info, 'required', True),
454
+ "default": getattr(field_info, 'default', None) if getattr(field_info, 'default', ...) is not ... else None,
455
+ "constraints": {}
456
+ }
457
+
458
+ info["fields"][field_name] = field_data
459
+
460
+ if field_data["required"]:
461
+ info["required_fields"].append(field_name)
462
+ else:
463
+ info["optional_fields"].append(field_name)
464
+
465
+ return info
466
+
467
+
468
+ def validate_schema_compatibility(schema_str: str) -> Dict[str, Any]:
469
+ """
470
+ Validate that a string schema is compatible with Pydantic model generation.
471
+
472
+ Args:
473
+ schema_str: String schema definition to validate
474
+
475
+ Returns:
476
+ Dictionary with compatibility information and any warnings
477
+ """
478
+ from .parsing.string_parser import validate_string_schema
479
+
480
+ result = validate_string_schema(schema_str)
481
+
482
+ # Add Pydantic-specific compatibility checks
483
+ compatibility = {
484
+ "pydantic_compatible": result["valid"],
485
+ "warnings": result.get("warnings", []),
486
+ "errors": result.get("errors", []),
487
+ "features_used": result.get("features_used", []),
488
+ "recommendations": []
489
+ }
490
+
491
+ # Add recommendations for better Pydantic usage
492
+ if "arrays" in compatibility["features_used"]:
493
+ compatibility["recommendations"].append("Consider using List[Model] type hints for better IDE support")
494
+
495
+ if "union_types" in compatibility["features_used"]:
496
+ compatibility["recommendations"].append("Union types work well with Pydantic's automatic type coercion")
497
+
498
+ if "special_types" in compatibility["features_used"]:
499
+ compatibility["recommendations"].append("Special types (email, url, etc.) provide automatic validation")
500
+
501
+ return compatibility