string-schema 0.1.2__py3-none-any.whl → 0.1.4__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.
- string_schema/core/__init__.py +23 -0
- string_schema/core/builders.py +244 -0
- string_schema/core/fields.py +138 -0
- string_schema/core/validators.py +242 -0
- string_schema/examples/__init__.py +36 -0
- string_schema/examples/presets.py +345 -0
- string_schema/examples/recipes.py +380 -0
- string_schema/integrations/__init__.py +15 -0
- string_schema/integrations/json_schema.py +385 -0
- string_schema/integrations/openapi.py +484 -0
- string_schema/integrations/pydantic.py +662 -0
- string_schema/integrations/reverse.py +275 -0
- string_schema/parsing/__init__.py +16 -0
- string_schema/parsing/optimizer.py +246 -0
- string_schema/parsing/string_parser.py +703 -0
- string_schema/parsing/syntax.py +250 -0
- string_schema/utilities.py +3 -3
- {string_schema-0.1.2.dist-info → string_schema-0.1.4.dist-info}/METADATA +1 -1
- string_schema-0.1.4.dist-info/RECORD +24 -0
- string_schema-0.1.2.dist-info/RECORD +0 -8
- {string_schema-0.1.2.dist-info → string_schema-0.1.4.dist-info}/WHEEL +0 -0
- {string_schema-0.1.2.dist-info → string_schema-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {string_schema-0.1.2.dist-info → string_schema-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic integration for Simple Schema
|
|
3
|
+
|
|
4
|
+
Contains functions for creating Pydantic models from Simple Schema definitions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional, Union, Type
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from ..core.fields import SimpleField
|
|
11
|
+
|
|
12
|
+
# Optional pydantic import
|
|
13
|
+
try:
|
|
14
|
+
from pydantic import BaseModel, Field, create_model
|
|
15
|
+
HAS_PYDANTIC = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
HAS_PYDANTIC = False
|
|
18
|
+
BaseModel = None
|
|
19
|
+
Field = None
|
|
20
|
+
create_model = None
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_pydantic_model(name: str, fields: Dict[str, Union[str, SimpleField]],
|
|
26
|
+
base_class: Type = None) -> Type:
|
|
27
|
+
"""
|
|
28
|
+
Create Pydantic model from Simple Schema field definitions.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name: Name of the model class
|
|
32
|
+
fields: Dictionary of field definitions
|
|
33
|
+
base_class: Base class to inherit from (default: BaseModel)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dynamically created Pydantic model class
|
|
37
|
+
"""
|
|
38
|
+
if not HAS_PYDANTIC:
|
|
39
|
+
raise ImportError("Pydantic is required for create_pydantic_model. Install with: pip install pydantic")
|
|
40
|
+
|
|
41
|
+
if base_class is None:
|
|
42
|
+
base_class = BaseModel
|
|
43
|
+
|
|
44
|
+
pydantic_fields = {}
|
|
45
|
+
|
|
46
|
+
for field_name, field_def in fields.items():
|
|
47
|
+
if isinstance(field_def, str):
|
|
48
|
+
field_def = SimpleField(field_def)
|
|
49
|
+
|
|
50
|
+
python_type, field_info = _simple_field_to_pydantic(field_def)
|
|
51
|
+
pydantic_fields[field_name] = (python_type, field_info)
|
|
52
|
+
|
|
53
|
+
# Create the model with the specified base class
|
|
54
|
+
return create_model(name, __base__=base_class, **pydantic_fields)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _simple_field_to_pydantic(field: SimpleField) -> tuple:
|
|
58
|
+
"""Convert SimpleField to Pydantic field specification"""
|
|
59
|
+
if not HAS_PYDANTIC:
|
|
60
|
+
raise ImportError("Pydantic is required for this function")
|
|
61
|
+
|
|
62
|
+
# Type mapping
|
|
63
|
+
type_mapping = {
|
|
64
|
+
'string': str,
|
|
65
|
+
'integer': int,
|
|
66
|
+
'number': float,
|
|
67
|
+
'boolean': bool
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
python_type = type_mapping.get(field.field_type, str)
|
|
71
|
+
|
|
72
|
+
# Handle union types
|
|
73
|
+
if field.union_types and len(field.union_types) > 1:
|
|
74
|
+
from typing import Union as TypingUnion
|
|
75
|
+
union_python_types = []
|
|
76
|
+
for union_type in field.union_types:
|
|
77
|
+
if union_type == "null":
|
|
78
|
+
union_python_types.append(type(None))
|
|
79
|
+
else:
|
|
80
|
+
union_python_types.append(type_mapping.get(union_type, str))
|
|
81
|
+
python_type = TypingUnion[tuple(union_python_types)]
|
|
82
|
+
|
|
83
|
+
# Handle optional fields
|
|
84
|
+
if not field.required:
|
|
85
|
+
python_type = Optional[python_type]
|
|
86
|
+
|
|
87
|
+
# Build Field arguments
|
|
88
|
+
field_kwargs = {}
|
|
89
|
+
|
|
90
|
+
if field.description:
|
|
91
|
+
field_kwargs['description'] = field.description
|
|
92
|
+
|
|
93
|
+
if field.default is not None:
|
|
94
|
+
field_kwargs['default'] = field.default
|
|
95
|
+
elif not field.required:
|
|
96
|
+
field_kwargs['default'] = None
|
|
97
|
+
|
|
98
|
+
# Numeric constraints
|
|
99
|
+
if field.min_val is not None:
|
|
100
|
+
field_kwargs['ge'] = field.min_val
|
|
101
|
+
if field.max_val is not None:
|
|
102
|
+
field_kwargs['le'] = field.max_val
|
|
103
|
+
|
|
104
|
+
# String constraints
|
|
105
|
+
if field.min_length is not None:
|
|
106
|
+
field_kwargs['min_length'] = field.min_length
|
|
107
|
+
if field.max_length is not None:
|
|
108
|
+
field_kwargs['max_length'] = field.max_length
|
|
109
|
+
|
|
110
|
+
return python_type, Field(**field_kwargs) if field_kwargs else Field()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# New consistent naming
|
|
114
|
+
def json_schema_to_model(json_schema: Dict[str, Any], name: str) -> Type[BaseModel]:
|
|
115
|
+
"""
|
|
116
|
+
Create Pydantic model from JSON Schema dictionary.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
json_schema: JSON Schema dictionary
|
|
120
|
+
name: Name of the model class
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dynamically created Pydantic model class
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
UserModel = json_schema_to_model(json_schema, 'User')
|
|
127
|
+
"""
|
|
128
|
+
return create_pydantic_from_json_schema(json_schema, name)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Legacy alias for backward compatibility
|
|
132
|
+
def json_schema_to_pydantic(json_schema: Dict[str, Any], name: str) -> Type[BaseModel]:
|
|
133
|
+
"""
|
|
134
|
+
Create Pydantic model from JSON Schema dictionary.
|
|
135
|
+
|
|
136
|
+
DEPRECATED: Use json_schema_to_model() instead for consistent naming.
|
|
137
|
+
"""
|
|
138
|
+
return json_schema_to_model(json_schema, name)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def create_pydantic_from_json_schema(json_schema: Dict[str, Any], name: str) -> Type[BaseModel]:
|
|
142
|
+
"""
|
|
143
|
+
Create Pydantic model from JSON Schema.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
json_schema: JSON Schema dictionary
|
|
147
|
+
name: Name of the model class
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dynamically created Pydantic model class
|
|
151
|
+
"""
|
|
152
|
+
if json_schema.get('type') != 'object':
|
|
153
|
+
raise ValueError("JSON Schema must be of type 'object' to create Pydantic model")
|
|
154
|
+
|
|
155
|
+
properties = json_schema.get('properties', {})
|
|
156
|
+
required_fields = set(json_schema.get('required', []))
|
|
157
|
+
|
|
158
|
+
pydantic_fields = {}
|
|
159
|
+
|
|
160
|
+
for field_name, field_schema in properties.items():
|
|
161
|
+
python_type, field_info = _json_schema_to_pydantic_field(field_schema, field_name in required_fields, f"{name}{field_name.title()}")
|
|
162
|
+
pydantic_fields[field_name] = (python_type, field_info)
|
|
163
|
+
|
|
164
|
+
return create_model(name, **pydantic_fields)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _json_schema_to_pydantic_field(field_schema: Dict[str, Any], required: bool = True, parent_name: str = "Nested") -> tuple:
|
|
168
|
+
"""Convert JSON Schema field to Pydantic field specification"""
|
|
169
|
+
# Type mapping
|
|
170
|
+
type_mapping = {
|
|
171
|
+
'string': str,
|
|
172
|
+
'integer': int,
|
|
173
|
+
'number': float,
|
|
174
|
+
'boolean': bool
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Handle union types
|
|
178
|
+
if 'anyOf' in field_schema:
|
|
179
|
+
from typing import Union as TypingUnion
|
|
180
|
+
union_types = []
|
|
181
|
+
for union_option in field_schema['anyOf']:
|
|
182
|
+
option_type = type_mapping.get(union_option.get('type', 'string'), str)
|
|
183
|
+
union_types.append(option_type)
|
|
184
|
+
python_type = TypingUnion[tuple(union_types)]
|
|
185
|
+
elif field_schema.get('type') == 'object':
|
|
186
|
+
# Handle nested objects by creating a nested Pydantic model
|
|
187
|
+
nested_model = create_pydantic_from_json_schema(field_schema, f"{parent_name}Nested")
|
|
188
|
+
python_type = nested_model
|
|
189
|
+
elif field_schema.get('type') == 'array':
|
|
190
|
+
# Handle arrays
|
|
191
|
+
items_schema = field_schema.get('items', {})
|
|
192
|
+
if items_schema.get('type') == 'object':
|
|
193
|
+
# Array of objects
|
|
194
|
+
from typing import List
|
|
195
|
+
item_model = create_pydantic_from_json_schema(items_schema, f"{parent_name}Item")
|
|
196
|
+
python_type = List[item_model]
|
|
197
|
+
else:
|
|
198
|
+
# Array of primitives
|
|
199
|
+
from typing import List
|
|
200
|
+
item_type = type_mapping.get(items_schema.get('type', 'string'), str)
|
|
201
|
+
python_type = List[item_type]
|
|
202
|
+
else:
|
|
203
|
+
field_type = field_schema.get('type', 'string')
|
|
204
|
+
python_type = type_mapping.get(field_type, str)
|
|
205
|
+
|
|
206
|
+
# Handle optional fields
|
|
207
|
+
if not required:
|
|
208
|
+
python_type = Optional[python_type]
|
|
209
|
+
|
|
210
|
+
# Build Field arguments
|
|
211
|
+
field_kwargs = {}
|
|
212
|
+
|
|
213
|
+
if 'description' in field_schema:
|
|
214
|
+
field_kwargs['description'] = field_schema['description']
|
|
215
|
+
|
|
216
|
+
if 'default' in field_schema:
|
|
217
|
+
field_kwargs['default'] = field_schema['default']
|
|
218
|
+
elif not required:
|
|
219
|
+
field_kwargs['default'] = None
|
|
220
|
+
|
|
221
|
+
# Numeric constraints
|
|
222
|
+
if 'minimum' in field_schema:
|
|
223
|
+
field_kwargs['ge'] = field_schema['minimum']
|
|
224
|
+
if 'maximum' in field_schema:
|
|
225
|
+
field_kwargs['le'] = field_schema['maximum']
|
|
226
|
+
|
|
227
|
+
# String constraints
|
|
228
|
+
if 'minLength' in field_schema:
|
|
229
|
+
field_kwargs['min_length'] = field_schema['minLength']
|
|
230
|
+
if 'maxLength' in field_schema:
|
|
231
|
+
field_kwargs['max_length'] = field_schema['maxLength']
|
|
232
|
+
|
|
233
|
+
# Format constraints (email, url, etc.)
|
|
234
|
+
if 'format' in field_schema:
|
|
235
|
+
format_type = field_schema['format']
|
|
236
|
+
if format_type == 'email':
|
|
237
|
+
# Use EmailStr for email validation
|
|
238
|
+
try:
|
|
239
|
+
from pydantic import EmailStr
|
|
240
|
+
python_type = EmailStr
|
|
241
|
+
if not required:
|
|
242
|
+
python_type = Optional[EmailStr]
|
|
243
|
+
except ImportError:
|
|
244
|
+
# Fallback to string with pattern validation
|
|
245
|
+
field_kwargs['pattern'] = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
246
|
+
elif format_type in ['uri', 'url']:
|
|
247
|
+
# Use HttpUrl for URL validation
|
|
248
|
+
try:
|
|
249
|
+
from pydantic import HttpUrl
|
|
250
|
+
python_type = HttpUrl
|
|
251
|
+
if not required:
|
|
252
|
+
python_type = Optional[HttpUrl]
|
|
253
|
+
except ImportError:
|
|
254
|
+
# Fallback to string
|
|
255
|
+
pass
|
|
256
|
+
elif format_type == 'uuid':
|
|
257
|
+
# Use UUID type for UUID validation
|
|
258
|
+
try:
|
|
259
|
+
from uuid import UUID
|
|
260
|
+
python_type = UUID
|
|
261
|
+
if not required:
|
|
262
|
+
python_type = Optional[UUID]
|
|
263
|
+
except ImportError:
|
|
264
|
+
# Fallback to string
|
|
265
|
+
pass
|
|
266
|
+
elif format_type == 'date-time':
|
|
267
|
+
# Use datetime for datetime validation
|
|
268
|
+
try:
|
|
269
|
+
from datetime import datetime
|
|
270
|
+
python_type = datetime
|
|
271
|
+
if not required:
|
|
272
|
+
python_type = Optional[datetime]
|
|
273
|
+
except ImportError:
|
|
274
|
+
# Fallback to string
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
# Array constraints
|
|
278
|
+
if 'minItems' in field_schema:
|
|
279
|
+
field_kwargs['min_length'] = field_schema['minItems']
|
|
280
|
+
if 'maxItems' in field_schema:
|
|
281
|
+
field_kwargs['max_length'] = field_schema['maxItems']
|
|
282
|
+
|
|
283
|
+
# Enum constraints
|
|
284
|
+
if 'enum' in field_schema:
|
|
285
|
+
# Create a Literal type for enum validation
|
|
286
|
+
from typing import Literal
|
|
287
|
+
enum_values = field_schema['enum']
|
|
288
|
+
if len(enum_values) == 1:
|
|
289
|
+
python_type = Literal[enum_values[0]]
|
|
290
|
+
else:
|
|
291
|
+
python_type = Literal[tuple(enum_values)]
|
|
292
|
+
|
|
293
|
+
# Handle optional enum fields
|
|
294
|
+
if not required:
|
|
295
|
+
python_type = Optional[python_type]
|
|
296
|
+
|
|
297
|
+
return python_type, Field(**field_kwargs) if field_kwargs else Field()
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def model_to_simple_fields(model: Type[BaseModel]) -> Dict[str, SimpleField]:
|
|
301
|
+
"""
|
|
302
|
+
Convert Pydantic model to Simple Schema field definitions.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
model: Pydantic model class
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
Dictionary of SimpleField objects
|
|
309
|
+
"""
|
|
310
|
+
fields = {}
|
|
311
|
+
|
|
312
|
+
for field_name, field_info in model.model_fields.items():
|
|
313
|
+
simple_field = _pydantic_field_to_simple_field(field_info, field_name)
|
|
314
|
+
fields[field_name] = simple_field
|
|
315
|
+
|
|
316
|
+
return fields
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _pydantic_field_to_simple_field(field_info: Any, field_name: str) -> SimpleField:
|
|
320
|
+
"""Convert Pydantic field info to SimpleField"""
|
|
321
|
+
# This is a simplified conversion - could be enhanced
|
|
322
|
+
|
|
323
|
+
# Determine field type
|
|
324
|
+
annotation = getattr(field_info, 'annotation', str)
|
|
325
|
+
|
|
326
|
+
if annotation == str:
|
|
327
|
+
field_type = 'string'
|
|
328
|
+
elif annotation == int:
|
|
329
|
+
field_type = 'integer'
|
|
330
|
+
elif annotation == float:
|
|
331
|
+
field_type = 'number'
|
|
332
|
+
elif annotation == bool:
|
|
333
|
+
field_type = 'boolean'
|
|
334
|
+
else:
|
|
335
|
+
# Handle Optional and Union types
|
|
336
|
+
if hasattr(annotation, '__origin__'):
|
|
337
|
+
if annotation.__origin__ is Union:
|
|
338
|
+
# Union type - use first non-None type
|
|
339
|
+
args = annotation.__args__
|
|
340
|
+
non_none_types = [arg for arg in args if arg is not type(None)]
|
|
341
|
+
if non_none_types:
|
|
342
|
+
first_type = non_none_types[0]
|
|
343
|
+
if first_type == str:
|
|
344
|
+
field_type = 'string'
|
|
345
|
+
elif first_type == int:
|
|
346
|
+
field_type = 'integer'
|
|
347
|
+
elif first_type == float:
|
|
348
|
+
field_type = 'number'
|
|
349
|
+
elif first_type == bool:
|
|
350
|
+
field_type = 'boolean'
|
|
351
|
+
else:
|
|
352
|
+
field_type = 'string'
|
|
353
|
+
else:
|
|
354
|
+
field_type = 'string'
|
|
355
|
+
else:
|
|
356
|
+
field_type = 'string'
|
|
357
|
+
else:
|
|
358
|
+
field_type = 'string'
|
|
359
|
+
|
|
360
|
+
# Determine if required
|
|
361
|
+
required = field_info.is_required() if hasattr(field_info, 'is_required') else True
|
|
362
|
+
|
|
363
|
+
# Get default value
|
|
364
|
+
default = getattr(field_info, 'default', None)
|
|
365
|
+
if default is ...: # Ellipsis indicates no default
|
|
366
|
+
default = None
|
|
367
|
+
|
|
368
|
+
# Get description
|
|
369
|
+
description = getattr(field_info, 'description', '')
|
|
370
|
+
|
|
371
|
+
# Create SimpleField
|
|
372
|
+
simple_field = SimpleField(
|
|
373
|
+
field_type=field_type,
|
|
374
|
+
description=description,
|
|
375
|
+
required=required,
|
|
376
|
+
default=default
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return simple_field
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def validate_pydantic_compatibility(fields: Dict[str, SimpleField]) -> Dict[str, Any]:
|
|
383
|
+
"""
|
|
384
|
+
Validate that Simple Schema fields are compatible with Pydantic.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
fields: Dictionary of SimpleField objects
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Validation result dictionary
|
|
391
|
+
"""
|
|
392
|
+
result = {
|
|
393
|
+
'compatible': True,
|
|
394
|
+
'warnings': [],
|
|
395
|
+
'errors': []
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
for field_name, field in fields.items():
|
|
399
|
+
# Check for unsupported features
|
|
400
|
+
if field.union_types and len(field.union_types) > 2:
|
|
401
|
+
result['warnings'].append(f"Field '{field_name}' has complex union type - may need manual handling")
|
|
402
|
+
|
|
403
|
+
if field.format_hint and field.format_hint not in ['email', 'url', 'uuid']:
|
|
404
|
+
result['warnings'].append(f"Field '{field_name}' format hint '{field.format_hint}' may not be fully supported")
|
|
405
|
+
|
|
406
|
+
# Check for conflicting constraints
|
|
407
|
+
if field.min_val is not None and field.max_val is not None:
|
|
408
|
+
if field.min_val > field.max_val:
|
|
409
|
+
result['errors'].append(f"Field '{field_name}' has min_val > max_val")
|
|
410
|
+
|
|
411
|
+
if field.min_length is not None and field.max_length is not None:
|
|
412
|
+
if field.min_length > field.max_length:
|
|
413
|
+
result['errors'].append(f"Field '{field_name}' has min_length > max_length")
|
|
414
|
+
|
|
415
|
+
result['compatible'] = len(result['errors']) == 0
|
|
416
|
+
return result
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _string_to_model_with_name(name: str, schema_str: str) -> Type[BaseModel]:
|
|
420
|
+
"""
|
|
421
|
+
Create Pydantic model directly from string syntax with explicit name.
|
|
422
|
+
|
|
423
|
+
Internal function - use string_to_model from utilities instead.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
name: Name of the model class
|
|
427
|
+
schema_str: String schema definition (e.g., "name:string, email:email")
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Dynamically created Pydantic model class
|
|
431
|
+
|
|
432
|
+
Example:
|
|
433
|
+
UserModel = _string_to_model_with_name('User', "name:string, email:email, age:int?")
|
|
434
|
+
user = UserModel(name="John", email="john@example.com")
|
|
435
|
+
"""
|
|
436
|
+
if not HAS_PYDANTIC:
|
|
437
|
+
raise ImportError("Pydantic is required for _string_to_model_with_name. Install with: pip install pydantic")
|
|
438
|
+
|
|
439
|
+
# Import here to avoid circular imports
|
|
440
|
+
from ..parsing.string_parser import parse_string_schema
|
|
441
|
+
|
|
442
|
+
# Convert string to JSON Schema, then to Pydantic
|
|
443
|
+
json_schema = parse_string_schema(schema_str)
|
|
444
|
+
return create_pydantic_from_json_schema(json_schema, name)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
# Legacy alias for backward compatibility
|
|
448
|
+
def string_to_pydantic(name: str, schema_str: str) -> Type[BaseModel]:
|
|
449
|
+
"""
|
|
450
|
+
Create Pydantic model directly from string syntax.
|
|
451
|
+
|
|
452
|
+
DEPRECATED: Use string_to_model() instead for consistent naming.
|
|
453
|
+
"""
|
|
454
|
+
return _string_to_model_with_name(name, schema_str)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def string_to_model_code(name: str, schema_str: str) -> str:
|
|
458
|
+
"""
|
|
459
|
+
Generate Pydantic model code directly from string syntax.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
name: Name of the model class
|
|
463
|
+
schema_str: String schema definition (e.g., "name:string, email:email")
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
Python code string for the Pydantic model
|
|
467
|
+
|
|
468
|
+
Example:
|
|
469
|
+
code = string_to_model_code('User', "name:string, email:email, age:int?")
|
|
470
|
+
print(code)
|
|
471
|
+
# Output:
|
|
472
|
+
# from pydantic import BaseModel, Field
|
|
473
|
+
# from typing import Optional, Union
|
|
474
|
+
#
|
|
475
|
+
# class User(BaseModel):
|
|
476
|
+
# name: str
|
|
477
|
+
# email: str = Field(format='email')
|
|
478
|
+
# age: Optional[int] = None
|
|
479
|
+
"""
|
|
480
|
+
# Import here to avoid circular imports
|
|
481
|
+
from ..parsing.string_parser import parse_string_schema
|
|
482
|
+
|
|
483
|
+
# Convert string to JSON Schema, then to SimpleField objects, then to code
|
|
484
|
+
json_schema = parse_string_schema(schema_str)
|
|
485
|
+
|
|
486
|
+
# Convert JSON Schema back to SimpleField objects for code generation
|
|
487
|
+
# This is a bit roundabout, but maintains compatibility with existing code
|
|
488
|
+
properties = json_schema.get('properties', {})
|
|
489
|
+
required_fields = set(json_schema.get('required', []))
|
|
490
|
+
|
|
491
|
+
fields = {}
|
|
492
|
+
for field_name, field_schema in properties.items():
|
|
493
|
+
# Convert JSON Schema property back to SimpleField
|
|
494
|
+
simple_field = _json_schema_to_simple_field(field_schema, field_name in required_fields)
|
|
495
|
+
fields[field_name] = simple_field
|
|
496
|
+
|
|
497
|
+
return generate_pydantic_code(name, fields)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# Legacy alias for backward compatibility
|
|
501
|
+
def string_to_pydantic_code(name: str, schema_str: str) -> str:
|
|
502
|
+
"""
|
|
503
|
+
Generate Pydantic model code directly from string syntax.
|
|
504
|
+
|
|
505
|
+
DEPRECATED: Use string_to_model_code() instead for consistent naming.
|
|
506
|
+
"""
|
|
507
|
+
return string_to_model_code(name, schema_str)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# Reverse conversion functions
|
|
511
|
+
def model_to_string(model: Type[BaseModel]) -> str:
|
|
512
|
+
"""
|
|
513
|
+
Convert Pydantic model to Simple Schema string syntax.
|
|
514
|
+
|
|
515
|
+
Args:
|
|
516
|
+
model: Pydantic model class
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
String representation in Simple Schema syntax
|
|
520
|
+
|
|
521
|
+
Example:
|
|
522
|
+
UserModel = string_to_model("name:string, email:email, age:int?")
|
|
523
|
+
schema_str = model_to_string(UserModel)
|
|
524
|
+
# Returns: "name:string, email:email, age:int?"
|
|
525
|
+
"""
|
|
526
|
+
from .reverse import model_to_string as _model_to_string
|
|
527
|
+
return _model_to_string(model)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def model_to_json_schema(model: Type[BaseModel]) -> Dict[str, Any]:
|
|
531
|
+
"""
|
|
532
|
+
Convert Pydantic model to JSON Schema.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
model: Pydantic model class
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
JSON Schema dictionary
|
|
539
|
+
|
|
540
|
+
Example:
|
|
541
|
+
UserModel = string_to_model("name:string, email:email")
|
|
542
|
+
json_schema = model_to_json_schema(UserModel)
|
|
543
|
+
"""
|
|
544
|
+
from .reverse import model_to_json_schema as _model_to_json_schema
|
|
545
|
+
return _model_to_json_schema(model)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _json_schema_to_simple_field(field_schema: Dict[str, Any], required: bool) -> SimpleField:
|
|
549
|
+
"""Convert JSON Schema property back to SimpleField for code generation"""
|
|
550
|
+
from ..core.fields import SimpleField
|
|
551
|
+
|
|
552
|
+
field_type = field_schema.get('type', 'string')
|
|
553
|
+
format_hint = field_schema.get('format')
|
|
554
|
+
|
|
555
|
+
# Handle constraints
|
|
556
|
+
kwargs = {
|
|
557
|
+
'required': required,
|
|
558
|
+
'description': field_schema.get('description', ''),
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if 'minimum' in field_schema:
|
|
562
|
+
kwargs['min_val'] = field_schema['minimum']
|
|
563
|
+
if 'maximum' in field_schema:
|
|
564
|
+
kwargs['max_val'] = field_schema['maximum']
|
|
565
|
+
if 'minLength' in field_schema:
|
|
566
|
+
kwargs['min_length'] = field_schema['minLength']
|
|
567
|
+
if 'maxLength' in field_schema:
|
|
568
|
+
kwargs['max_length'] = field_schema['maxLength']
|
|
569
|
+
if 'enum' in field_schema:
|
|
570
|
+
kwargs['choices'] = field_schema['enum']
|
|
571
|
+
if format_hint:
|
|
572
|
+
kwargs['format_hint'] = format_hint
|
|
573
|
+
|
|
574
|
+
return SimpleField(field_type, **kwargs)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def generate_pydantic_code(name: str, fields: Dict[str, SimpleField]) -> str:
|
|
578
|
+
"""
|
|
579
|
+
Generate Pydantic model code as a string.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
name: Name of the model class
|
|
583
|
+
fields: Dictionary of SimpleField objects
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Python code string for the Pydantic model
|
|
587
|
+
"""
|
|
588
|
+
lines = [
|
|
589
|
+
"from pydantic import BaseModel, Field",
|
|
590
|
+
"from typing import Optional, Union",
|
|
591
|
+
"",
|
|
592
|
+
f"class {name}(BaseModel):"
|
|
593
|
+
]
|
|
594
|
+
|
|
595
|
+
if not fields:
|
|
596
|
+
lines.append(" pass")
|
|
597
|
+
return "\n".join(lines)
|
|
598
|
+
|
|
599
|
+
for field_name, field in fields.items():
|
|
600
|
+
field_line = _generate_pydantic_field_code(field_name, field)
|
|
601
|
+
lines.append(f" {field_line}")
|
|
602
|
+
|
|
603
|
+
return "\n".join(lines)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _generate_pydantic_field_code(field_name: str, field: SimpleField) -> str:
|
|
607
|
+
"""Generate code for a single Pydantic field"""
|
|
608
|
+
# Determine Python type
|
|
609
|
+
type_mapping = {
|
|
610
|
+
'string': 'str',
|
|
611
|
+
'integer': 'int',
|
|
612
|
+
'number': 'float',
|
|
613
|
+
'boolean': 'bool'
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
python_type = type_mapping.get(field.field_type, 'str')
|
|
617
|
+
|
|
618
|
+
# Handle union types
|
|
619
|
+
if field.union_types and len(field.union_types) > 1:
|
|
620
|
+
union_types = []
|
|
621
|
+
for union_type in field.union_types:
|
|
622
|
+
if union_type == 'null':
|
|
623
|
+
union_types.append('None')
|
|
624
|
+
else:
|
|
625
|
+
union_types.append(type_mapping.get(union_type, 'str'))
|
|
626
|
+
python_type = f"Union[{', '.join(union_types)}]"
|
|
627
|
+
|
|
628
|
+
# Handle optional
|
|
629
|
+
if not field.required:
|
|
630
|
+
if 'Union' not in python_type:
|
|
631
|
+
python_type = f"Optional[{python_type}]"
|
|
632
|
+
|
|
633
|
+
# Build field definition
|
|
634
|
+
field_parts = []
|
|
635
|
+
|
|
636
|
+
if field.description:
|
|
637
|
+
field_parts.append(f"description='{field.description}'")
|
|
638
|
+
|
|
639
|
+
if field.default is not None:
|
|
640
|
+
if isinstance(field.default, str):
|
|
641
|
+
field_parts.append(f"default='{field.default}'")
|
|
642
|
+
else:
|
|
643
|
+
field_parts.append(f"default={field.default}")
|
|
644
|
+
elif not field.required:
|
|
645
|
+
field_parts.append("default=None")
|
|
646
|
+
|
|
647
|
+
# Add constraints
|
|
648
|
+
if field.min_val is not None:
|
|
649
|
+
field_parts.append(f"ge={field.min_val}")
|
|
650
|
+
if field.max_val is not None:
|
|
651
|
+
field_parts.append(f"le={field.max_val}")
|
|
652
|
+
if field.min_length is not None:
|
|
653
|
+
field_parts.append(f"min_length={field.min_length}")
|
|
654
|
+
if field.max_length is not None:
|
|
655
|
+
field_parts.append(f"max_length={field.max_length}")
|
|
656
|
+
|
|
657
|
+
if field_parts:
|
|
658
|
+
field_def = f"Field({', '.join(field_parts)})"
|
|
659
|
+
else:
|
|
660
|
+
field_def = "Field()"
|
|
661
|
+
|
|
662
|
+
return f"{field_name}: {python_type} = {field_def}"
|