string-schema 0.1.2__py3-none-any.whl → 0.1.3__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,23 @@
1
+ """
2
+ Core module for String Schema
3
+
4
+ Contains the fundamental classes and functions for schema definition and building.
5
+ """
6
+
7
+ from .fields import SimpleField
8
+ from .builders import (
9
+ simple_schema,
10
+ list_of_objects_schema,
11
+ simple_array_schema,
12
+ quick_pydantic_model
13
+ )
14
+ from .validators import validate_schema
15
+
16
+ __all__ = [
17
+ "SimpleField",
18
+ "simple_schema",
19
+ "list_of_objects_schema",
20
+ "simple_array_schema",
21
+ "quick_pydantic_model",
22
+ "validate_schema"
23
+ ]
@@ -0,0 +1,244 @@
1
+ """
2
+ Schema builders for Simple Schema
3
+
4
+ Contains functions for building schemas from SimpleField definitions.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, Union, Type
8
+ import logging
9
+
10
+ from .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 simple_schema(fields: Dict[str, Union[str, SimpleField]]) -> Dict[str, Any]:
26
+ """Generate JSON Schema from simple field definitions with enhanced support"""
27
+ properties = {}
28
+ required = []
29
+
30
+ for field_name, field_def in fields.items():
31
+ if isinstance(field_def, str):
32
+ field_def = SimpleField(field_def)
33
+
34
+ prop_schema = _simple_field_to_json_schema(field_def)
35
+ properties[field_name] = prop_schema
36
+
37
+ if field_def.required:
38
+ required.append(field_name)
39
+
40
+ schema = {
41
+ "type": "object",
42
+ "properties": properties
43
+ }
44
+ if required:
45
+ schema["required"] = required
46
+
47
+ return schema
48
+
49
+
50
+ def _simple_field_to_json_schema(field: SimpleField) -> Dict[str, Any]:
51
+ """Convert SimpleField to JSON Schema property with enhanced features"""
52
+ # Handle union types first
53
+ if field.union_types and len(field.union_types) > 1:
54
+ union_schemas = []
55
+ for union_type in field.union_types:
56
+ if union_type == "null":
57
+ union_schemas.append({"type": "null"})
58
+ else:
59
+ union_schemas.append({"type": union_type})
60
+ prop = {"anyOf": union_schemas}
61
+
62
+ # Add description to the union
63
+ if field.description:
64
+ prop["description"] = field.description
65
+
66
+ return prop
67
+
68
+ # Regular single type
69
+ prop = {"type": field.field_type}
70
+
71
+ if field.description:
72
+ prop["description"] = field.description
73
+ if field.default is not None:
74
+ prop["default"] = field.default
75
+
76
+ # Handle enum/choices
77
+ if field.choices:
78
+ prop["enum"] = field.choices
79
+
80
+ # Add format hints for special types
81
+ if field.format_hint:
82
+ if field.format_hint == "email":
83
+ prop["format"] = "email"
84
+ elif field.format_hint in ["url", "uri"]:
85
+ prop["format"] = "uri"
86
+ elif field.format_hint == "datetime":
87
+ prop["format"] = "date-time"
88
+ elif field.format_hint == "date":
89
+ prop["format"] = "date"
90
+ elif field.format_hint == "uuid":
91
+ prop["format"] = "uuid"
92
+ # Note: phone doesn't have a standard JSON Schema format
93
+
94
+ # Numeric constraints
95
+ if field.field_type in ["integer", "number"]:
96
+ if field.min_val is not None:
97
+ prop["minimum"] = field.min_val
98
+ if field.max_val is not None:
99
+ prop["maximum"] = field.max_val
100
+
101
+ # String constraints
102
+ if field.field_type == "string":
103
+ if field.min_length is not None:
104
+ prop["minLength"] = field.min_length
105
+ if field.max_length is not None:
106
+ prop["maxLength"] = field.max_length
107
+
108
+ # Array constraints (for when this field itself is an array)
109
+ if field.min_items is not None:
110
+ prop["minItems"] = field.min_items
111
+ if field.max_items is not None:
112
+ prop["maxItems"] = field.max_items
113
+
114
+ return prop
115
+
116
+
117
+ def list_of_objects_schema(item_fields: Dict[str, Union[str, SimpleField]],
118
+ description: str = "List of objects",
119
+ min_items: Optional[int] = None,
120
+ max_items: Optional[int] = None) -> Dict[str, Any]:
121
+ """Generate schema for array of objects with enhanced constraints"""
122
+ item_schema = simple_schema(item_fields)
123
+
124
+ array_schema = {
125
+ "type": "array",
126
+ "description": description,
127
+ "items": item_schema
128
+ }
129
+
130
+ # Add array size constraints
131
+ if min_items is not None:
132
+ array_schema["minItems"] = min_items
133
+ if max_items is not None:
134
+ array_schema["maxItems"] = max_items
135
+
136
+ return array_schema
137
+
138
+
139
+ def simple_array_schema(item_type: str = 'string', description: str = 'Array of items',
140
+ min_items: Optional[int] = None, max_items: Optional[int] = None,
141
+ format_hint: Optional[str] = None) -> Dict[str, Any]:
142
+ """Generate schema for simple arrays like [string], [int], [email]"""
143
+ items_schema = {"type": item_type}
144
+
145
+ # Add format for special types
146
+ if format_hint:
147
+ if format_hint == "email":
148
+ items_schema["format"] = "email"
149
+ elif format_hint in ["url", "uri"]:
150
+ items_schema["format"] = "uri"
151
+ elif format_hint == "datetime":
152
+ items_schema["format"] = "date-time"
153
+ elif format_hint == "date":
154
+ items_schema["format"] = "date"
155
+ elif format_hint == "uuid":
156
+ items_schema["format"] = "uuid"
157
+
158
+ array_schema = {
159
+ "type": "array",
160
+ "description": description,
161
+ "items": items_schema
162
+ }
163
+
164
+ # Add array size constraints
165
+ if min_items is not None:
166
+ array_schema["minItems"] = min_items
167
+ if max_items is not None:
168
+ array_schema["maxItems"] = max_items
169
+
170
+ return array_schema
171
+
172
+
173
+ def quick_pydantic_model(name: str, fields: Dict[str, Union[str, SimpleField]]) -> Type:
174
+ """Create Pydantic model from simple field definitions"""
175
+ if not HAS_PYDANTIC:
176
+ raise ImportError("Pydantic is required for quick_pydantic_model. Install with: pip install pydantic")
177
+
178
+ pydantic_fields = {}
179
+
180
+ for field_name, field_def in fields.items():
181
+ if isinstance(field_def, str):
182
+ field_def = SimpleField(field_def)
183
+
184
+ from ..integrations.pydantic import _simple_field_to_pydantic
185
+ python_type, field_info = _simple_field_to_pydantic(field_def)
186
+ pydantic_fields[field_name] = (python_type, field_info)
187
+
188
+ return create_model(name, **pydantic_fields)
189
+
190
+
191
+ def _simple_field_to_pydantic(field: SimpleField) -> tuple:
192
+ """Convert SimpleField to Pydantic field specification"""
193
+ if not HAS_PYDANTIC:
194
+ raise ImportError("Pydantic is required for this function")
195
+
196
+ # Type mapping
197
+ type_mapping = {
198
+ 'string': str,
199
+ 'integer': int,
200
+ 'number': float,
201
+ 'boolean': bool
202
+ }
203
+
204
+ python_type = type_mapping.get(field.field_type, str)
205
+
206
+ # Handle union types
207
+ if field.union_types and len(field.union_types) > 1:
208
+ from typing import Union as TypingUnion
209
+ union_python_types = []
210
+ for union_type in field.union_types:
211
+ if union_type == "null":
212
+ union_python_types.append(type(None))
213
+ else:
214
+ union_python_types.append(type_mapping.get(union_type, str))
215
+ python_type = TypingUnion[tuple(union_python_types)]
216
+
217
+ # Handle optional fields
218
+ if not field.required:
219
+ python_type = Optional[python_type]
220
+
221
+ # Build Field arguments
222
+ field_kwargs = {}
223
+
224
+ if field.description:
225
+ field_kwargs['description'] = field.description
226
+
227
+ if field.default is not None:
228
+ field_kwargs['default'] = field.default
229
+ elif not field.required:
230
+ field_kwargs['default'] = None
231
+
232
+ # Numeric constraints
233
+ if field.min_val is not None:
234
+ field_kwargs['ge'] = field.min_val
235
+ if field.max_val is not None:
236
+ field_kwargs['le'] = field.max_val
237
+
238
+ # String constraints
239
+ if field.min_length is not None:
240
+ field_kwargs['min_length'] = field.min_length
241
+ if field.max_length is not None:
242
+ field_kwargs['max_length'] = field.max_length
243
+
244
+ return python_type, Field(**field_kwargs) if field_kwargs else Field()
@@ -0,0 +1,138 @@
1
+ """
2
+ Core field definitions for Simple Schema
3
+
4
+ Contains the SimpleField class and related field utilities.
5
+ """
6
+
7
+ from typing import Any, List, Optional, Union
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class SimpleField:
14
+ """Enhanced SimpleField with support for all new features"""
15
+
16
+ def __init__(self, field_type: str, description: str = "", required: bool = True,
17
+ default: Any = None, min_val: Optional[Union[int, float]] = None,
18
+ max_val: Optional[Union[int, float]] = None, min_length: Optional[int] = None,
19
+ max_length: Optional[int] = None, choices: Optional[List[Any]] = None,
20
+ min_items: Optional[int] = None, max_items: Optional[int] = None,
21
+ format_hint: Optional[str] = None, union_types: Optional[List[str]] = None):
22
+ """
23
+ Initialize a SimpleField with comprehensive validation options.
24
+
25
+ Args:
26
+ field_type: The base type (string, integer, number, boolean)
27
+ description: Human-readable description of the field
28
+ required: Whether the field is required (default: True)
29
+ default: Default value if field is not provided
30
+ min_val: Minimum value for numeric types
31
+ max_val: Maximum value for numeric types
32
+ min_length: Minimum length for string types
33
+ max_length: Maximum length for string types
34
+ choices: List of allowed values (enum)
35
+ min_items: Minimum items for array types
36
+ max_items: Maximum items for array types
37
+ format_hint: Special format hint (email, url, datetime, etc.)
38
+ union_types: List of types for union fields
39
+ """
40
+ self.field_type = field_type
41
+ self.description = description
42
+ self.required = required
43
+ self.default = default
44
+ self.min_val = min_val
45
+ self.max_val = max_val
46
+ self.min_length = min_length
47
+ self.max_length = max_length
48
+ self.choices = choices
49
+ self.min_items = min_items
50
+ self.max_items = max_items
51
+ self.format_hint = format_hint
52
+ self.union_types = union_types or []
53
+
54
+ def __repr__(self):
55
+ """String representation of the field"""
56
+ parts = [f"type={self.field_type}"]
57
+ if self.description:
58
+ parts.append(f"desc='{self.description}'")
59
+ if not self.required:
60
+ parts.append("optional")
61
+ if self.choices:
62
+ parts.append(f"choices={self.choices}")
63
+ if self.union_types:
64
+ parts.append(f"union={self.union_types}")
65
+ return f"SimpleField({', '.join(parts)})"
66
+
67
+ def to_dict(self) -> dict:
68
+ """Convert field to dictionary representation"""
69
+ result = {
70
+ 'type': self.field_type,
71
+ 'required': self.required
72
+ }
73
+
74
+ if self.description:
75
+ result['description'] = self.description
76
+ if self.default is not None:
77
+ result['default'] = self.default
78
+ if self.min_val is not None:
79
+ result['min_val'] = self.min_val
80
+ if self.max_val is not None:
81
+ result['max_val'] = self.max_val
82
+ if self.min_length is not None:
83
+ result['min_length'] = self.min_length
84
+ if self.max_length is not None:
85
+ result['max_length'] = self.max_length
86
+ if self.choices:
87
+ result['choices'] = self.choices
88
+ if self.min_items is not None:
89
+ result['min_items'] = self.min_items
90
+ if self.max_items is not None:
91
+ result['max_items'] = self.max_items
92
+ if self.format_hint:
93
+ result['format_hint'] = self.format_hint
94
+ if self.union_types:
95
+ result['union_types'] = self.union_types
96
+
97
+ return result
98
+
99
+ @classmethod
100
+ def from_dict(cls, data: dict) -> 'SimpleField':
101
+ """Create SimpleField from dictionary representation"""
102
+ return cls(
103
+ field_type=data['type'],
104
+ description=data.get('description', ''),
105
+ required=data.get('required', True),
106
+ default=data.get('default'),
107
+ min_val=data.get('min_val'),
108
+ max_val=data.get('max_val'),
109
+ min_length=data.get('min_length'),
110
+ max_length=data.get('max_length'),
111
+ choices=data.get('choices'),
112
+ min_items=data.get('min_items'),
113
+ max_items=data.get('max_items'),
114
+ format_hint=data.get('format_hint'),
115
+ union_types=data.get('union_types')
116
+ )
117
+
118
+
119
+ # Utility functions for creating common field types
120
+ def create_enhanced_field(field_type: str, **kwargs) -> SimpleField:
121
+ """Create enhanced SimpleField with all features"""
122
+ return SimpleField(field_type, **kwargs)
123
+
124
+
125
+ def create_special_type_field(special_type: str, required: bool = True, **kwargs) -> SimpleField:
126
+ """Create field with special type hints"""
127
+ return SimpleField('string', format_hint=special_type, required=required, **kwargs)
128
+
129
+
130
+ def create_enum_field(values: List[str], required: bool = True, **kwargs) -> SimpleField:
131
+ """Create enum field with specific values"""
132
+ return SimpleField('string', choices=values, required=required, **kwargs)
133
+
134
+
135
+ def create_union_field(types: List[str], required: bool = True, **kwargs) -> SimpleField:
136
+ """Create union field with multiple types"""
137
+ primary_type = types[0] if types else 'string'
138
+ return SimpleField(primary_type, union_types=types, required=required, **kwargs)
@@ -0,0 +1,242 @@
1
+ """
2
+ Validation utilities for Simple Schema
3
+
4
+ Contains functions for validating schemas and field definitions.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Union
8
+ import logging
9
+
10
+ from .fields import SimpleField
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def validate_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
16
+ """
17
+ Validate a JSON schema generated by Simple Schema.
18
+
19
+ Args:
20
+ schema: The JSON schema to validate
21
+
22
+ Returns:
23
+ Dictionary with validation results including:
24
+ - valid: Boolean indicating if schema is valid
25
+ - errors: List of error messages
26
+ - warnings: List of warning messages
27
+ - field_count: Number of fields in the schema
28
+ """
29
+ result = {
30
+ 'valid': True,
31
+ 'errors': [],
32
+ 'warnings': [],
33
+ 'field_count': 0,
34
+ 'features_used': []
35
+ }
36
+
37
+ try:
38
+ # Basic schema structure validation
39
+ if not isinstance(schema, dict):
40
+ result['errors'].append("Schema must be a dictionary")
41
+ result['valid'] = False
42
+ return result
43
+
44
+ if 'type' not in schema:
45
+ result['errors'].append("Schema must have a 'type' field")
46
+ result['valid'] = False
47
+ return result
48
+
49
+ schema_type = schema['type']
50
+
51
+ if schema_type == 'object':
52
+ result.update(_validate_object_schema(schema))
53
+ elif schema_type == 'array':
54
+ result.update(_validate_array_schema(schema))
55
+ else:
56
+ result['warnings'].append(f"Unusual schema type: {schema_type}")
57
+
58
+ # Check for potential issues
59
+ if result['field_count'] > 20:
60
+ result['warnings'].append("Schema has many fields (>20), consider simplifying")
61
+
62
+ if result['field_count'] == 0:
63
+ result['warnings'].append("Schema has no fields defined")
64
+
65
+ except Exception as e:
66
+ result['errors'].append(f"Validation error: {str(e)}")
67
+ result['valid'] = False
68
+
69
+ # Set overall validity
70
+ result['valid'] = len(result['errors']) == 0
71
+
72
+ return result
73
+
74
+
75
+ def _validate_object_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
76
+ """Validate object-type schema"""
77
+ result = {
78
+ 'field_count': 0,
79
+ 'features_used': [],
80
+ 'errors': [],
81
+ 'warnings': []
82
+ }
83
+
84
+ properties = schema.get('properties', {})
85
+ required = schema.get('required', [])
86
+
87
+ result['field_count'] = len(properties)
88
+
89
+ # Validate each property
90
+ for field_name, field_schema in properties.items():
91
+ field_result = _validate_field_schema(field_name, field_schema)
92
+ result['errors'].extend(field_result['errors'])
93
+ result['warnings'].extend(field_result['warnings'])
94
+ result['features_used'].extend(field_result['features_used'])
95
+
96
+ # Validate required fields
97
+ for req_field in required:
98
+ if req_field not in properties:
99
+ result['errors'].append(f"Required field '{req_field}' not found in properties")
100
+
101
+ # Remove duplicate features
102
+ result['features_used'] = list(set(result['features_used']))
103
+
104
+ return result
105
+
106
+
107
+ def _validate_array_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
108
+ """Validate array-type schema"""
109
+ result = {
110
+ 'field_count': 0,
111
+ 'features_used': ['arrays'],
112
+ 'errors': [],
113
+ 'warnings': []
114
+ }
115
+
116
+ items = schema.get('items', {})
117
+
118
+ if not items:
119
+ result['errors'].append("Array schema must have 'items' definition")
120
+ return result
121
+
122
+ # Validate array constraints
123
+ if 'minItems' in schema:
124
+ result['features_used'].append('array_constraints')
125
+ if 'maxItems' in schema:
126
+ result['features_used'].append('array_constraints')
127
+
128
+ # Validate items schema
129
+ if items.get('type') == 'object':
130
+ items_result = _validate_object_schema(items)
131
+ result['field_count'] = items_result['field_count']
132
+ result['errors'].extend(items_result['errors'])
133
+ result['warnings'].extend(items_result['warnings'])
134
+ result['features_used'].extend(items_result['features_used'])
135
+ else:
136
+ # Simple array items
137
+ field_result = _validate_field_schema('array_item', items)
138
+ result['errors'].extend(field_result['errors'])
139
+ result['warnings'].extend(field_result['warnings'])
140
+ result['features_used'].extend(field_result['features_used'])
141
+
142
+ return result
143
+
144
+
145
+ def _validate_field_schema(field_name: str, field_schema: Dict[str, Any]) -> Dict[str, Any]:
146
+ """Validate individual field schema"""
147
+ result = {
148
+ 'features_used': [],
149
+ 'errors': [],
150
+ 'warnings': []
151
+ }
152
+
153
+ # Check for union types
154
+ if 'anyOf' in field_schema:
155
+ result['features_used'].append('union_types')
156
+ # Validate each union option
157
+ for i, union_option in enumerate(field_schema['anyOf']):
158
+ if 'type' not in union_option:
159
+ result['errors'].append(f"Union option {i} in field '{field_name}' missing type")
160
+
161
+ # Check for enum
162
+ if 'enum' in field_schema:
163
+ result['features_used'].append('enums')
164
+ enum_values = field_schema['enum']
165
+ if not isinstance(enum_values, list) or len(enum_values) == 0:
166
+ result['errors'].append(f"Field '{field_name}' enum must be non-empty list")
167
+
168
+ # Check for format hints
169
+ if 'format' in field_schema:
170
+ result['features_used'].append('special_types')
171
+ format_value = field_schema['format']
172
+ valid_formats = ['email', 'uri', 'date-time', 'date', 'uuid']
173
+ if format_value not in valid_formats:
174
+ result['warnings'].append(f"Field '{field_name}' uses non-standard format: {format_value}")
175
+
176
+ # Check for constraints
177
+ constraint_fields = ['minimum', 'maximum', 'minLength', 'maxLength', 'minItems', 'maxItems']
178
+ if any(field in field_schema for field in constraint_fields):
179
+ result['features_used'].append('constraints')
180
+
181
+ # Validate constraint values
182
+ if 'minimum' in field_schema and 'maximum' in field_schema:
183
+ if field_schema['minimum'] > field_schema['maximum']:
184
+ result['errors'].append(f"Field '{field_name}' minimum > maximum")
185
+
186
+ if 'minLength' in field_schema and 'maxLength' in field_schema:
187
+ if field_schema['minLength'] > field_schema['maxLength']:
188
+ result['errors'].append(f"Field '{field_name}' minLength > maxLength")
189
+
190
+ if 'minItems' in field_schema and 'maxItems' in field_schema:
191
+ if field_schema['minItems'] > field_schema['maxItems']:
192
+ result['errors'].append(f"Field '{field_name}' minItems > maxItems")
193
+
194
+ return result
195
+
196
+
197
+ def validate_simple_field(field: SimpleField) -> Dict[str, Any]:
198
+ """
199
+ Validate a SimpleField instance.
200
+
201
+ Args:
202
+ field: The SimpleField to validate
203
+
204
+ Returns:
205
+ Dictionary with validation results
206
+ """
207
+ result = {
208
+ 'valid': True,
209
+ 'errors': [],
210
+ 'warnings': []
211
+ }
212
+
213
+ # Validate field type
214
+ valid_types = ['string', 'integer', 'number', 'boolean']
215
+ if field.field_type not in valid_types:
216
+ result['warnings'].append(f"Unusual field type: {field.field_type}")
217
+
218
+ # Validate constraints
219
+ if field.min_val is not None and field.max_val is not None:
220
+ if field.min_val > field.max_val:
221
+ result['errors'].append("min_val cannot be greater than max_val")
222
+
223
+ if field.min_length is not None and field.max_length is not None:
224
+ if field.min_length > field.max_length:
225
+ result['errors'].append("min_length cannot be greater than max_length")
226
+
227
+ if field.min_items is not None and field.max_items is not None:
228
+ if field.min_items > field.max_items:
229
+ result['errors'].append("min_items cannot be greater than max_items")
230
+
231
+ # Validate choices
232
+ if field.choices and len(field.choices) == 0:
233
+ result['errors'].append("choices list cannot be empty")
234
+
235
+ # Validate union types
236
+ if field.union_types and len(field.union_types) < 2:
237
+ result['warnings'].append("union_types should have at least 2 types")
238
+
239
+ # Set overall validity
240
+ result['valid'] = len(result['errors']) == 0
241
+
242
+ return result
@@ -0,0 +1,36 @@
1
+ """
2
+ Examples module for String Schema
3
+
4
+ Contains built-in schema presets and common patterns.
5
+ """
6
+
7
+ from .presets import (
8
+ user_schema,
9
+ product_schema,
10
+ contact_schema,
11
+ article_schema,
12
+ event_schema,
13
+ get_examples
14
+ )
15
+ from .recipes import (
16
+ create_list_schema,
17
+ create_nested_schema,
18
+ create_enum_schema,
19
+ create_union_schema
20
+ )
21
+
22
+ __all__ = [
23
+ # Presets
24
+ "user_schema",
25
+ "product_schema",
26
+ "contact_schema",
27
+ "article_schema",
28
+ "event_schema",
29
+ "get_examples",
30
+
31
+ # Recipes
32
+ "create_list_schema",
33
+ "create_nested_schema",
34
+ "create_enum_schema",
35
+ "create_union_schema"
36
+ ]