string-schema 0.1.6__tar.gz → 0.1.7__tar.gz
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-0.1.6/string_schema.egg-info → string_schema-0.1.7}/PKG-INFO +1 -1
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/string-syntax.md +50 -0
- string_schema-0.1.7/examples/defaults_descriptions_demo.py +203 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/pyproject.toml +1 -1
- {string_schema-0.1.6 → string_schema-0.1.7}/setup.py +1 -1
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/__init__.py +1 -1
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/core/builders.py +2 -2
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/core/fields.py +12 -7
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/integrations/pydantic.py +1 -1
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/parsing/string_parser.py +143 -18
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/utilities.py +1 -1
- {string_schema-0.1.6 → string_schema-0.1.7/string_schema.egg-info}/PKG-INFO +1 -1
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema.egg-info/SOURCES.txt +1 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/LICENSE +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/MANIFEST.in +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/README.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/README.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/advanced-usage.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/api-reference.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/examples.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/faq.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/getting-started.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/pydantic-utilities.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/docs/troubleshooting.md +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/examples/demo.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/examples/pydantic_utility_demo.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/setup.cfg +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/core/__init__.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/core/validators.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/examples/__init__.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/examples/presets.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/examples/recipes.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/integrations/__init__.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/integrations/json_schema.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/integrations/openapi.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/integrations/reverse.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/parsing/__init__.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/parsing/optimizer.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/parsing/syntax.py +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema/py.typed +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema.egg-info/dependency_links.txt +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema.egg-info/requires.txt +0 -0
- {string_schema-0.1.6 → string_schema-0.1.7}/string_schema.egg-info/top_level.txt +0 -0
|
@@ -40,6 +40,56 @@ price:number(min=0)
|
|
|
40
40
|
description:text(max=500)
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
### Default Values
|
|
44
|
+
|
|
45
|
+
Add default values using `=`:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
active:bool=true
|
|
49
|
+
count:int=0
|
|
50
|
+
status:string=pending
|
|
51
|
+
price:number=9.99
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Default values can be combined with constraints:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
age:int(0,120)=18
|
|
58
|
+
limit:int(1,1000)=100
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Field Descriptions
|
|
62
|
+
|
|
63
|
+
Add human-readable descriptions using ` |` (space before pipe):
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
name:string | User's full name
|
|
67
|
+
age:int | User's age in years
|
|
68
|
+
email:email | Contact email address
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Combined Syntax
|
|
72
|
+
|
|
73
|
+
You can combine all features:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
name:string(min=1,max=100) | User's full name
|
|
77
|
+
age:int(0,120)=18 | User's age in years
|
|
78
|
+
active:bool=true | Whether the account is active
|
|
79
|
+
email:string? | Optional email address
|
|
80
|
+
count:int(1,1000)=1 | Number of items to process
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Syntax Order:**
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
field_name:type(constraints)?=default | description
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `?` = optional marker (before `=`)
|
|
90
|
+
- `=value` = default value
|
|
91
|
+
- ` | text` = description (space before pipe)
|
|
92
|
+
|
|
43
93
|
## Type System
|
|
44
94
|
|
|
45
95
|
### Basic Types
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Demo: Enhanced String Schema with Defaults and Descriptions
|
|
3
|
+
|
|
4
|
+
This example demonstrates the new syntax for defining default values
|
|
5
|
+
and descriptions in string-schema.
|
|
6
|
+
|
|
7
|
+
New syntax:
|
|
8
|
+
- field:type=default
|
|
9
|
+
- field:type | description
|
|
10
|
+
- field:type=default | description
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from string_schema import parse_string_schema, validate_to_dict
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def print_section(title):
|
|
18
|
+
"""Print a formatted section header"""
|
|
19
|
+
print(f"\n{'='*60}")
|
|
20
|
+
print(f" {title}")
|
|
21
|
+
print('='*60)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def demo_default_values():
|
|
25
|
+
"""Demonstrate default value syntax"""
|
|
26
|
+
print_section("Default Values")
|
|
27
|
+
|
|
28
|
+
# Define schema with defaults
|
|
29
|
+
schema_str = """
|
|
30
|
+
active:bool=true,
|
|
31
|
+
count:int=0,
|
|
32
|
+
status:string=pending,
|
|
33
|
+
price:number=9.99
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
print("\nSchema definition:")
|
|
37
|
+
print(schema_str)
|
|
38
|
+
|
|
39
|
+
# Parse schema
|
|
40
|
+
schema = parse_string_schema(schema_str)
|
|
41
|
+
print("\nGenerated JSON Schema:")
|
|
42
|
+
print(json.dumps(schema, indent=2))
|
|
43
|
+
|
|
44
|
+
# Validate data with defaults
|
|
45
|
+
print("\n--- Test 1: Empty data (uses all defaults) ---")
|
|
46
|
+
data = {}
|
|
47
|
+
result = validate_to_dict(data, schema_str)
|
|
48
|
+
print(f"Input: {data}")
|
|
49
|
+
print(f"Output: {result}")
|
|
50
|
+
|
|
51
|
+
print("\n--- Test 2: Partial data (some defaults) ---")
|
|
52
|
+
data = {"count": 5}
|
|
53
|
+
result = validate_to_dict(data, schema_str)
|
|
54
|
+
print(f"Input: {data}")
|
|
55
|
+
print(f"Output: {result}")
|
|
56
|
+
|
|
57
|
+
print("\n--- Test 3: Override defaults ---")
|
|
58
|
+
data = {"active": False, "count": 10, "status": "completed", "price": 19.99}
|
|
59
|
+
result = validate_to_dict(data, schema_str)
|
|
60
|
+
print(f"Input: {data}")
|
|
61
|
+
print(f"Output: {result}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def demo_descriptions():
|
|
65
|
+
"""Demonstrate description syntax"""
|
|
66
|
+
print_section("Field Descriptions")
|
|
67
|
+
|
|
68
|
+
# Define schema with descriptions
|
|
69
|
+
schema_str = """
|
|
70
|
+
name:string | User's full name,
|
|
71
|
+
age:int | User's age in years,
|
|
72
|
+
email:email | Contact email address,
|
|
73
|
+
created:datetime | Account creation timestamp
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
print("\nSchema definition:")
|
|
77
|
+
print(schema_str)
|
|
78
|
+
|
|
79
|
+
# Parse schema
|
|
80
|
+
schema = parse_string_schema(schema_str)
|
|
81
|
+
print("\nGenerated JSON Schema (with descriptions):")
|
|
82
|
+
print(json.dumps(schema, indent=2))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def demo_combined_syntax():
|
|
86
|
+
"""Demonstrate combined defaults and descriptions"""
|
|
87
|
+
print_section("Combined: Defaults + Descriptions")
|
|
88
|
+
|
|
89
|
+
# Define schema with both defaults and descriptions
|
|
90
|
+
schema_str = """
|
|
91
|
+
name:string | User's full name,
|
|
92
|
+
age:int(0,120)=18 | User's age in years,
|
|
93
|
+
active:bool=true | Whether the account is active,
|
|
94
|
+
email:string? | Optional email address,
|
|
95
|
+
count:int(1,1000)=1 | Number of items to process
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
print("\nSchema definition:")
|
|
99
|
+
print(schema_str)
|
|
100
|
+
|
|
101
|
+
# Parse schema
|
|
102
|
+
schema = parse_string_schema(schema_str)
|
|
103
|
+
print("\nGenerated JSON Schema:")
|
|
104
|
+
print(json.dumps(schema, indent=2))
|
|
105
|
+
|
|
106
|
+
# Validate data
|
|
107
|
+
print("\n--- Test: Minimal data (uses defaults) ---")
|
|
108
|
+
data = {"name": "John Doe"}
|
|
109
|
+
result = validate_to_dict(data, schema_str)
|
|
110
|
+
print(f"Input: {data}")
|
|
111
|
+
print(f"Output: {result}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def demo_worker_parameters():
|
|
115
|
+
"""Demonstrate using enhanced syntax for worker parameters"""
|
|
116
|
+
print_section("Real-World Example: Worker Parameters")
|
|
117
|
+
|
|
118
|
+
# Define worker parameters schema
|
|
119
|
+
schema_str = """
|
|
120
|
+
enable_cleanup:bool=true | Enable article cleanup step,
|
|
121
|
+
enable_cat_tag:bool=true | Enable category and tag assignment,
|
|
122
|
+
enable_create_summary:bool=true | Enable summary generation,
|
|
123
|
+
enable_embedding_generation:bool=true | Enable embedding generation,
|
|
124
|
+
max_articles_per_iteration:int(1,1000)=1 | Maximum articles to process per run,
|
|
125
|
+
processing_days_limit:int(1,365)=7 | Number of days to look back for articles,
|
|
126
|
+
model_name:string? | LLM model to use (leave empty for default)
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
print("\nWorker Parameters Schema:")
|
|
130
|
+
print(schema_str)
|
|
131
|
+
|
|
132
|
+
# Parse schema
|
|
133
|
+
schema = parse_string_schema(schema_str)
|
|
134
|
+
print("\nGenerated JSON Schema:")
|
|
135
|
+
print(json.dumps(schema, indent=2))
|
|
136
|
+
|
|
137
|
+
# Example 1: Default configuration
|
|
138
|
+
print("\n--- Example 1: Default configuration ---")
|
|
139
|
+
params = {}
|
|
140
|
+
result = validate_to_dict(params, schema_str)
|
|
141
|
+
print(f"Input: {params}")
|
|
142
|
+
print(f"Output: {json.dumps(result, indent=2)}")
|
|
143
|
+
|
|
144
|
+
# Example 2: Custom configuration
|
|
145
|
+
print("\n--- Example 2: Custom configuration ---")
|
|
146
|
+
params = {
|
|
147
|
+
"enable_cleanup": False,
|
|
148
|
+
"max_articles_per_iteration": 10,
|
|
149
|
+
"model_name": "openrouter:google/gemini-2.5-flash-lite"
|
|
150
|
+
}
|
|
151
|
+
result = validate_to_dict(params, schema_str)
|
|
152
|
+
print(f"Input: {json.dumps(params, indent=2)}")
|
|
153
|
+
print(f"Output: {json.dumps(result, indent=2)}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def demo_null_defaults():
|
|
157
|
+
"""Demonstrate null default values"""
|
|
158
|
+
print_section("Null Default Values")
|
|
159
|
+
|
|
160
|
+
# Define schema with null defaults
|
|
161
|
+
schema_str = """
|
|
162
|
+
name:string,
|
|
163
|
+
email:string?=null | Optional email (defaults to null),
|
|
164
|
+
phone:string?=null | Optional phone (defaults to null),
|
|
165
|
+
notes:string? | Optional notes (no default)
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
print("\nSchema definition:")
|
|
169
|
+
print(schema_str)
|
|
170
|
+
|
|
171
|
+
# Parse schema
|
|
172
|
+
schema = parse_string_schema(schema_str)
|
|
173
|
+
print("\nGenerated JSON Schema:")
|
|
174
|
+
print(json.dumps(schema, indent=2))
|
|
175
|
+
|
|
176
|
+
# Validate data
|
|
177
|
+
print("\n--- Test: Minimal data ---")
|
|
178
|
+
data = {"name": "John Doe"}
|
|
179
|
+
result = validate_to_dict(data, schema_str)
|
|
180
|
+
print(f"Input: {data}")
|
|
181
|
+
print(f"Output: {result}")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main():
|
|
185
|
+
"""Run all demos"""
|
|
186
|
+
print("\n" + "="*60)
|
|
187
|
+
print(" String Schema: Defaults and Descriptions Demo")
|
|
188
|
+
print("="*60)
|
|
189
|
+
|
|
190
|
+
demo_default_values()
|
|
191
|
+
demo_descriptions()
|
|
192
|
+
demo_combined_syntax()
|
|
193
|
+
demo_worker_parameters()
|
|
194
|
+
demo_null_defaults()
|
|
195
|
+
|
|
196
|
+
print("\n" + "="*60)
|
|
197
|
+
print(" Demo Complete!")
|
|
198
|
+
print("="*60 + "\n")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
main()
|
|
203
|
+
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "string-schema"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
description = "A simple, LLM-friendly schema definition library for converting string syntax to structured schemas"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -9,7 +9,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="string-schema",
|
|
12
|
-
version="0.1.
|
|
12
|
+
version="0.1.7",
|
|
13
13
|
author="Michael Chen",
|
|
14
14
|
author_email="xychen@msn.com",
|
|
15
15
|
description="A simple, LLM-friendly schema definition library for converting string syntax to structured schemas",
|
|
@@ -67,10 +67,10 @@ def _simple_field_to_json_schema(field: SimpleField) -> Dict[str, Any]:
|
|
|
67
67
|
|
|
68
68
|
# Regular single type
|
|
69
69
|
prop = {"type": field.field_type}
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
if field.description:
|
|
72
72
|
prop["description"] = field.description
|
|
73
|
-
if field.
|
|
73
|
+
if field.has_default:
|
|
74
74
|
prop["default"] = field.default
|
|
75
75
|
|
|
76
76
|
# Handle enum/choices
|
|
@@ -10,23 +10,27 @@ import logging
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
# Sentinel value to distinguish "no default" from "default is None"
|
|
14
|
+
_NO_DEFAULT = object()
|
|
15
|
+
|
|
16
|
+
|
|
13
17
|
class SimpleField:
|
|
14
18
|
"""Enhanced SimpleField with support for all new features"""
|
|
15
|
-
|
|
19
|
+
|
|
16
20
|
def __init__(self, field_type: str, description: str = "", required: bool = True,
|
|
17
|
-
default: Any =
|
|
21
|
+
default: Any = _NO_DEFAULT, min_val: Optional[Union[int, float]] = None,
|
|
18
22
|
max_val: Optional[Union[int, float]] = None, min_length: Optional[int] = None,
|
|
19
23
|
max_length: Optional[int] = None, choices: Optional[List[Any]] = None,
|
|
20
24
|
min_items: Optional[int] = None, max_items: Optional[int] = None,
|
|
21
25
|
format_hint: Optional[str] = None, union_types: Optional[List[str]] = None):
|
|
22
26
|
"""
|
|
23
27
|
Initialize a SimpleField with comprehensive validation options.
|
|
24
|
-
|
|
28
|
+
|
|
25
29
|
Args:
|
|
26
30
|
field_type: The base type (string, integer, number, boolean)
|
|
27
31
|
description: Human-readable description of the field
|
|
28
32
|
required: Whether the field is required (default: True)
|
|
29
|
-
default: Default value if field is not provided
|
|
33
|
+
default: Default value if field is not provided (use _NO_DEFAULT sentinel for no default)
|
|
30
34
|
min_val: Minimum value for numeric types
|
|
31
35
|
max_val: Maximum value for numeric types
|
|
32
36
|
min_length: Minimum length for string types
|
|
@@ -41,6 +45,7 @@ class SimpleField:
|
|
|
41
45
|
self.description = description
|
|
42
46
|
self.required = required
|
|
43
47
|
self.default = default
|
|
48
|
+
self.has_default = default is not _NO_DEFAULT
|
|
44
49
|
self.min_val = min_val
|
|
45
50
|
self.max_val = max_val
|
|
46
51
|
self.min_length = min_length
|
|
@@ -70,10 +75,10 @@ class SimpleField:
|
|
|
70
75
|
'type': self.field_type,
|
|
71
76
|
'required': self.required
|
|
72
77
|
}
|
|
73
|
-
|
|
78
|
+
|
|
74
79
|
if self.description:
|
|
75
80
|
result['description'] = self.description
|
|
76
|
-
if self.
|
|
81
|
+
if self.has_default:
|
|
77
82
|
result['default'] = self.default
|
|
78
83
|
if self.min_val is not None:
|
|
79
84
|
result['min_val'] = self.min_val
|
|
@@ -93,7 +98,7 @@ class SimpleField:
|
|
|
93
98
|
result['format_hint'] = self.format_hint
|
|
94
99
|
if self.union_types:
|
|
95
100
|
result['union_types'] = self.union_types
|
|
96
|
-
|
|
101
|
+
|
|
97
102
|
return result
|
|
98
103
|
|
|
99
104
|
@classmethod
|
|
@@ -90,7 +90,7 @@ def _simple_field_to_pydantic(field: SimpleField) -> tuple:
|
|
|
90
90
|
if field.description:
|
|
91
91
|
field_kwargs['description'] = field.description
|
|
92
92
|
|
|
93
|
-
if field.
|
|
93
|
+
if field.has_default:
|
|
94
94
|
field_kwargs['default'] = field.default
|
|
95
95
|
elif not field.required:
|
|
96
96
|
field_kwargs['default'] = None
|
|
@@ -8,7 +8,7 @@ import re
|
|
|
8
8
|
from typing import Dict, Any, List, Union, Optional
|
|
9
9
|
import logging
|
|
10
10
|
|
|
11
|
-
from ..core.fields import SimpleField
|
|
11
|
+
from ..core.fields import SimpleField, _NO_DEFAULT
|
|
12
12
|
from ..core.builders import simple_schema, list_of_objects_schema
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
@@ -136,52 +136,110 @@ def _parse_object_fields(fields_str: str) -> Dict[str, Any]:
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
def _parse_single_field_with_nesting(field_str: str) -> tuple:
|
|
139
|
-
"""
|
|
139
|
+
"""
|
|
140
|
+
Parse a single field with enhanced syntax support.
|
|
141
|
+
|
|
142
|
+
New syntax support:
|
|
143
|
+
- Descriptions: field:type | description
|
|
144
|
+
- Defaults: field:type=default
|
|
145
|
+
- Combined: field:type(constraints)=default | description
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
"name:string | User name"
|
|
149
|
+
"age:int=18"
|
|
150
|
+
"active:bool=true | Whether user is active"
|
|
151
|
+
"count:int(1,100)=1 | Item count"
|
|
152
|
+
"""
|
|
140
153
|
if not field_str:
|
|
141
154
|
return None, None
|
|
142
|
-
|
|
143
|
-
#
|
|
155
|
+
|
|
156
|
+
# Step 1: Extract description (after |)
|
|
157
|
+
# Note: Must be done BEFORE checking for union types (which also use |)
|
|
158
|
+
description = ""
|
|
159
|
+
# Only split on | if it's not part of a union type definition
|
|
160
|
+
# Union types are like "string|int|null" (no spaces, part of type definition)
|
|
161
|
+
# Descriptions are like "type | description" (with space before |)
|
|
162
|
+
# Look for " |" pattern to distinguish from union types
|
|
163
|
+
if ' |' in field_str:
|
|
164
|
+
# This is a description separator, not a union type
|
|
165
|
+
parts = field_str.split(' |', 1)
|
|
166
|
+
field_str = parts[0].strip()
|
|
167
|
+
if len(parts) > 1:
|
|
168
|
+
description = parts[1].strip()
|
|
169
|
+
|
|
170
|
+
# Step 2: Check for optional marker (?)
|
|
171
|
+
# Must be done BEFORE extracting default to handle "string?=null" correctly
|
|
144
172
|
required = True
|
|
145
|
-
if
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
if '?' in field_str:
|
|
174
|
+
# Check if ? is before = (e.g., "string?=null")
|
|
175
|
+
if '=' in field_str:
|
|
176
|
+
# Find position of ? and =
|
|
177
|
+
q_pos = field_str.find('?')
|
|
178
|
+
eq_pos = field_str.find('=')
|
|
179
|
+
if q_pos < eq_pos:
|
|
180
|
+
# ? comes before =, so it's an optional marker
|
|
181
|
+
field_str = field_str[:q_pos] + field_str[q_pos+1:]
|
|
182
|
+
required = False
|
|
183
|
+
elif field_str.endswith('?'):
|
|
184
|
+
# Simple optional marker at the end
|
|
185
|
+
required = False
|
|
186
|
+
field_str = field_str[:-1].strip()
|
|
187
|
+
|
|
188
|
+
# Step 3: Extract default value (after =)
|
|
189
|
+
# Must be done BEFORE parsing type definition to avoid conflicts with constraints
|
|
190
|
+
default_value = _NO_DEFAULT # Use sentinel to distinguish "no default" from "default is None"
|
|
191
|
+
if '=' in field_str:
|
|
192
|
+
# Check if = is after the type definition (not in constraints)
|
|
193
|
+
if ':' in field_str:
|
|
194
|
+
name_part, type_part = field_str.split(':', 1)
|
|
195
|
+
# Split type_part on = outside parentheses
|
|
196
|
+
parts = _split_on_equals_outside_parens(type_part)
|
|
197
|
+
if len(parts) == 2:
|
|
198
|
+
field_str = name_part + ':' + parts[0].strip()
|
|
199
|
+
default_str = parts[1].strip()
|
|
200
|
+
default_value = _parse_default_value(default_str)
|
|
201
|
+
|
|
202
|
+
# Step 4: Split field name and definition
|
|
150
203
|
if ':' in field_str:
|
|
151
204
|
field_name, field_def = field_str.split(':', 1)
|
|
152
205
|
field_name = field_name.strip()
|
|
153
206
|
field_def = field_def.strip()
|
|
154
|
-
|
|
207
|
+
|
|
155
208
|
# Handle nested structures
|
|
156
209
|
if field_def.startswith('[') or field_def.startswith('{'):
|
|
157
210
|
nested_structure = _parse_schema_structure(field_def)
|
|
158
211
|
nested_structure['required'] = required
|
|
159
212
|
return field_name, nested_structure
|
|
160
|
-
|
|
213
|
+
|
|
161
214
|
# Handle union types: string|int|null
|
|
215
|
+
# Union types have | without spaces around them
|
|
162
216
|
elif '|' in field_def:
|
|
163
217
|
union_types = [t.strip() for t in field_def.split('|')]
|
|
164
218
|
field_type = _normalize_type_name(union_types[0]) # Use first type as primary
|
|
165
|
-
|
|
219
|
+
|
|
166
220
|
# Create field with union support
|
|
167
221
|
field_obj = SimpleField(
|
|
168
222
|
field_type=field_type,
|
|
223
|
+
description=description,
|
|
224
|
+
default=default_value,
|
|
169
225
|
required=required
|
|
170
226
|
)
|
|
171
227
|
# Store union info for JSON schema generation
|
|
172
228
|
field_obj.union_types = [_normalize_type_name(t) for t in union_types]
|
|
173
229
|
return field_name, field_obj
|
|
174
|
-
|
|
230
|
+
|
|
175
231
|
# Handle enum types: enum(value1,value2,value3) or choice(...)
|
|
176
232
|
elif field_def.startswith(('enum(', 'choice(', 'select(')):
|
|
177
233
|
enum_values = _parse_enum_values(field_def)
|
|
178
234
|
field_obj = SimpleField(
|
|
179
235
|
field_type="string", # Enums are string-based
|
|
236
|
+
description=description,
|
|
237
|
+
default=default_value,
|
|
180
238
|
required=required,
|
|
181
239
|
choices=enum_values
|
|
182
240
|
)
|
|
183
241
|
return field_name, field_obj
|
|
184
|
-
|
|
242
|
+
|
|
185
243
|
# Handle array types: array(string,max=5) or list(int,min=1)
|
|
186
244
|
elif field_def.startswith(('array(', 'list(')):
|
|
187
245
|
array_type, constraints = _parse_array_type_definition(field_def)
|
|
@@ -195,7 +253,7 @@ def _parse_single_field_with_nesting(field_str: str) -> tuple:
|
|
|
195
253
|
"constraints": constraints,
|
|
196
254
|
"required": required
|
|
197
255
|
}
|
|
198
|
-
|
|
256
|
+
|
|
199
257
|
# Handle regular type with constraints
|
|
200
258
|
else:
|
|
201
259
|
original_type = field_def.split('(')[0].strip() # Get type before any constraints
|
|
@@ -208,16 +266,20 @@ def _parse_single_field_with_nesting(field_str: str) -> tuple:
|
|
|
208
266
|
|
|
209
267
|
field_obj = SimpleField(
|
|
210
268
|
field_type=field_type,
|
|
269
|
+
description=description,
|
|
270
|
+
default=default_value,
|
|
211
271
|
required=required,
|
|
212
272
|
**constraints
|
|
213
273
|
)
|
|
214
274
|
return field_name, field_obj
|
|
215
|
-
|
|
275
|
+
|
|
216
276
|
# Field name only (default to string)
|
|
217
277
|
else:
|
|
218
278
|
field_name = field_str.strip()
|
|
219
279
|
field_obj = SimpleField(
|
|
220
280
|
field_type="string",
|
|
281
|
+
description=description,
|
|
282
|
+
default=default_value,
|
|
221
283
|
required=required
|
|
222
284
|
)
|
|
223
285
|
return field_name, field_obj
|
|
@@ -229,12 +291,75 @@ def _parse_enum_values(enum_def: str) -> List[str]:
|
|
|
229
291
|
match = re.match(r'^(?:enum|choice|select)\(([^)]+)\)$', enum_def)
|
|
230
292
|
if not match:
|
|
231
293
|
return []
|
|
232
|
-
|
|
294
|
+
|
|
233
295
|
values_str = match.group(1)
|
|
234
296
|
values = [v.strip() for v in values_str.split(',')]
|
|
235
297
|
return values
|
|
236
298
|
|
|
237
299
|
|
|
300
|
+
def _split_on_equals_outside_parens(s: str) -> List[str]:
|
|
301
|
+
"""
|
|
302
|
+
Split string on FIRST = outside parentheses only.
|
|
303
|
+
|
|
304
|
+
Example:
|
|
305
|
+
"int(1,100)=5" -> ["int(1,100)", "5"]
|
|
306
|
+
"string=hello" -> ["string", "hello"]
|
|
307
|
+
"string=a=b" -> ["string", "a=b"] # Only split on first =
|
|
308
|
+
"""
|
|
309
|
+
depth = 0
|
|
310
|
+
|
|
311
|
+
for i, char in enumerate(s):
|
|
312
|
+
if char == '(':
|
|
313
|
+
depth += 1
|
|
314
|
+
elif char == ')':
|
|
315
|
+
depth -= 1
|
|
316
|
+
elif char == '=' and depth == 0:
|
|
317
|
+
# Found first = outside parentheses - split here
|
|
318
|
+
return [s[:i], s[i+1:]]
|
|
319
|
+
|
|
320
|
+
# No = found outside parentheses
|
|
321
|
+
return [s]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _parse_default_value(default_str: str) -> Any:
|
|
325
|
+
"""
|
|
326
|
+
Parse default value from string.
|
|
327
|
+
|
|
328
|
+
Supports:
|
|
329
|
+
- Booleans: true, false
|
|
330
|
+
- Null: null, none
|
|
331
|
+
- Numbers: 123, 45.67
|
|
332
|
+
- Strings: "hello", 'world', or unquoted
|
|
333
|
+
"""
|
|
334
|
+
default_str = default_str.strip()
|
|
335
|
+
|
|
336
|
+
# Boolean
|
|
337
|
+
if default_str.lower() in ['true', 'false']:
|
|
338
|
+
return default_str.lower() == 'true'
|
|
339
|
+
|
|
340
|
+
# Null/None
|
|
341
|
+
if default_str.lower() in ['null', 'none']:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
# Number
|
|
345
|
+
try:
|
|
346
|
+
if '.' in default_str:
|
|
347
|
+
return float(default_str)
|
|
348
|
+
else:
|
|
349
|
+
return int(default_str)
|
|
350
|
+
except ValueError:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
# String (remove quotes if present)
|
|
354
|
+
if default_str.startswith('"') and default_str.endswith('"'):
|
|
355
|
+
return default_str[1:-1]
|
|
356
|
+
if default_str.startswith("'") and default_str.endswith("'"):
|
|
357
|
+
return default_str[1:-1]
|
|
358
|
+
|
|
359
|
+
# Return as-is (string)
|
|
360
|
+
return default_str
|
|
361
|
+
|
|
362
|
+
|
|
238
363
|
def _parse_array_type_definition(array_def: str) -> tuple:
|
|
239
364
|
"""Parse array(type,constraints) or list(type,constraints)"""
|
|
240
365
|
# Extract content between parentheses
|
|
@@ -363,7 +488,7 @@ def _simple_field_to_json_schema(field: SimpleField) -> Dict[str, Any]:
|
|
|
363
488
|
# Basic metadata
|
|
364
489
|
if field.description:
|
|
365
490
|
prop["description"] = field.description
|
|
366
|
-
if field.
|
|
491
|
+
if field.has_default:
|
|
367
492
|
prop["default"] = field.default
|
|
368
493
|
|
|
369
494
|
# Handle union types
|
|
@@ -457,7 +457,7 @@ def get_model_info(model_class) -> Dict[str, Any]:
|
|
|
457
457
|
Returns:
|
|
458
458
|
Dictionary with model information including fields, types, and constraints
|
|
459
459
|
"""
|
|
460
|
-
if not HAS_PYDANTIC or not issubclass(model_class, BaseModel):
|
|
460
|
+
if not HAS_PYDANTIC or not isinstance(model_class, type) or not issubclass(model_class, BaseModel):
|
|
461
461
|
raise ValueError("Input must be a Pydantic model class")
|
|
462
462
|
|
|
463
463
|
info = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|