string-schema 0.1.5__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.
Files changed (43) hide show
  1. {string_schema-0.1.5/string_schema.egg-info → string_schema-0.1.7}/PKG-INFO +8 -1
  2. {string_schema-0.1.5 → string_schema-0.1.7}/README.md +7 -0
  3. {string_schema-0.1.5 → string_schema-0.1.7}/docs/pydantic-utilities.md +60 -0
  4. {string_schema-0.1.5 → string_schema-0.1.7}/docs/string-syntax.md +50 -0
  5. string_schema-0.1.7/examples/defaults_descriptions_demo.py +203 -0
  6. {string_schema-0.1.5 → string_schema-0.1.7}/pyproject.toml +1 -1
  7. {string_schema-0.1.5 → string_schema-0.1.7}/setup.py +1 -1
  8. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/__init__.py +1 -1
  9. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/core/builders.py +2 -2
  10. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/core/fields.py +12 -7
  11. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/integrations/pydantic.py +1 -1
  12. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/parsing/string_parser.py +143 -18
  13. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/utilities.py +62 -8
  14. {string_schema-0.1.5 → string_schema-0.1.7/string_schema.egg-info}/PKG-INFO +8 -1
  15. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema.egg-info/SOURCES.txt +1 -0
  16. {string_schema-0.1.5 → string_schema-0.1.7}/LICENSE +0 -0
  17. {string_schema-0.1.5 → string_schema-0.1.7}/MANIFEST.in +0 -0
  18. {string_schema-0.1.5 → string_schema-0.1.7}/docs/README.md +0 -0
  19. {string_schema-0.1.5 → string_schema-0.1.7}/docs/advanced-usage.md +0 -0
  20. {string_schema-0.1.5 → string_schema-0.1.7}/docs/api-reference.md +0 -0
  21. {string_schema-0.1.5 → string_schema-0.1.7}/docs/examples.md +0 -0
  22. {string_schema-0.1.5 → string_schema-0.1.7}/docs/faq.md +0 -0
  23. {string_schema-0.1.5 → string_schema-0.1.7}/docs/getting-started.md +0 -0
  24. {string_schema-0.1.5 → string_schema-0.1.7}/docs/troubleshooting.md +0 -0
  25. {string_schema-0.1.5 → string_schema-0.1.7}/examples/demo.py +0 -0
  26. {string_schema-0.1.5 → string_schema-0.1.7}/examples/pydantic_utility_demo.py +0 -0
  27. {string_schema-0.1.5 → string_schema-0.1.7}/setup.cfg +0 -0
  28. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/core/__init__.py +0 -0
  29. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/core/validators.py +0 -0
  30. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/examples/__init__.py +0 -0
  31. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/examples/presets.py +0 -0
  32. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/examples/recipes.py +0 -0
  33. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/integrations/__init__.py +0 -0
  34. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/integrations/json_schema.py +0 -0
  35. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/integrations/openapi.py +0 -0
  36. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/integrations/reverse.py +0 -0
  37. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/parsing/__init__.py +0 -0
  38. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/parsing/optimizer.py +0 -0
  39. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/parsing/syntax.py +0 -0
  40. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema/py.typed +0 -0
  41. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema.egg-info/dependency_links.txt +0 -0
  42. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema.egg-info/requires.txt +0 -0
  43. {string_schema-0.1.5 → string_schema-0.1.7}/string_schema.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: string-schema
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: A simple, LLM-friendly schema definition library for converting string syntax to structured schemas
5
5
  Home-page: https://github.com/xychenmsn/string-schema
6
6
  Author: Michael Chen
@@ -151,6 +151,7 @@ String Schema takes human-readable text descriptions and converts them into stru
151
151
  - **🤖 LLM Data Extraction**: Define extraction schemas that LLMs can easily follow
152
152
  - **🔧 API Development**: Generate Pydantic models and OpenAPI docs from simple syntax
153
153
  - **✅ Data Validation**: Create robust validation schemas with minimal code
154
+ - **🌍 Timezone-Aware APIs**: Automatic UTC conversion for consistent datetime handling
154
155
  - **📋 Configuration**: Define and validate application configuration schemas
155
156
  - **🔄 Data Transformation**: Convert between different schema formats
156
157
 
@@ -175,6 +176,12 @@ print(user_dict) # {"name": "John Doe", "email": "john@example.com", "age": 25}
175
176
  user_model = validate_to_model(raw_data, "name:string, email:email, age:int?")
176
177
  print(user_model.name) # "John Doe" - Full type safety
177
178
  print(user_model.age) # 25 - Converted to int
179
+
180
+ # 🌍 Automatic timezone handling for datetime fields
181
+ from datetime import datetime
182
+ event_data = {"name": "Meeting", "start_time": datetime(2025, 8, 13, 14, 30)}
183
+ event_dict = validate_to_dict(event_data, "name:string, start_time:datetime")
184
+ print(event_dict) # {"name": "Meeting", "start_time": "2025-08-13T14:30:00+00:00"}
178
185
  ```
179
186
 
180
187
  ### 🎨 Function Decorators
@@ -103,6 +103,7 @@ String Schema takes human-readable text descriptions and converts them into stru
103
103
  - **🤖 LLM Data Extraction**: Define extraction schemas that LLMs can easily follow
104
104
  - **🔧 API Development**: Generate Pydantic models and OpenAPI docs from simple syntax
105
105
  - **✅ Data Validation**: Create robust validation schemas with minimal code
106
+ - **🌍 Timezone-Aware APIs**: Automatic UTC conversion for consistent datetime handling
106
107
  - **📋 Configuration**: Define and validate application configuration schemas
107
108
  - **🔄 Data Transformation**: Convert between different schema formats
108
109
 
@@ -127,6 +128,12 @@ print(user_dict) # {"name": "John Doe", "email": "john@example.com", "age": 25}
127
128
  user_model = validate_to_model(raw_data, "name:string, email:email, age:int?")
128
129
  print(user_model.name) # "John Doe" - Full type safety
129
130
  print(user_model.age) # 25 - Converted to int
131
+
132
+ # 🌍 Automatic timezone handling for datetime fields
133
+ from datetime import datetime
134
+ event_data = {"name": "Meeting", "start_time": datetime(2025, 8, 13, 14, 30)}
135
+ event_dict = validate_to_dict(event_data, "name:string, start_time:datetime")
136
+ print(event_dict) # {"name": "Meeting", "start_time": "2025-08-13T14:30:00+00:00"}
130
137
  ```
131
138
 
132
139
  ### 🎨 Function Decorators
@@ -41,6 +41,7 @@ ProfileModel = string_to_model("name:string, profile:{bio:text?, avatar:url?, so
41
41
 
42
42
  - ✅ Basic types: `string`, `int`, `number`, `boolean`
43
43
  - ✅ Special types: `email`, `url`, `uuid`, `datetime`, `phone`
44
+ - ✅ **Timezone-aware datetime**: Automatic UTC conversion for consistent API responses
44
45
  - ✅ Arrays: `[string]`, `[{name:string, price:number}]`
45
46
  - ✅ Nested objects: `profile:{bio:text?, avatar:url?}`
46
47
  - ✅ Enums: `status:enum(active,inactive,pending)`
@@ -57,6 +58,11 @@ ProfileModel = string_to_model("name:string, profile:{bio:text?, avatar:url?, so
57
58
  user_dict = validate_to_dict(raw_data, "name:string, email:email, age:int?")
58
59
  # Returns: {"name": "John", "email": "john@example.com", "age": 30}
59
60
 
61
+ # Automatic timezone handling for datetime fields
62
+ event_data = {"name": "Meeting", "start_time": datetime(2025, 8, 13, 14, 30)}
63
+ event_dict = validate_to_dict(event_data, "name:string, start_time:datetime")
64
+ # Returns: {"name": "Meeting", "start_time": "2025-08-13T14:30:00+00:00"}
65
+
60
66
  # Array validation
61
67
  events = validate_to_dict(raw_events, "[{user_id:uuid, event:enum(login,logout,purchase)}]")
62
68
 
@@ -79,6 +85,60 @@ if profile.profile:
79
85
  print(profile.profile.bio)
80
86
  ```
81
87
 
88
+ ## 🌍 Timezone Handling
89
+
90
+ String Schema automatically ensures all datetime fields include timezone information for consistent, timezone-aware API responses.
91
+
92
+ ### **Automatic UTC Conversion**
93
+
94
+ ```python
95
+ from datetime import datetime
96
+ from string_schema import validate_to_dict
97
+
98
+ # Naive datetime gets UTC timezone
99
+ data = {"event": "Meeting", "time": datetime(2025, 8, 13, 14, 30)}
100
+ result = validate_to_dict(data, "event:string, time:datetime")
101
+ # Returns: {"event": "Meeting", "time": "2025-08-13T14:30:00+00:00"}
102
+
103
+ # Timezone-aware datetime preserved
104
+ from datetime import timezone
105
+ utc_time = datetime(2025, 8, 13, 14, 30, tzinfo=timezone.utc)
106
+ data = {"event": "UTC Meeting", "time": utc_time}
107
+ result = validate_to_dict(data, "event:string, time:datetime")
108
+ # Returns: {"event": "UTC Meeting", "time": "2025-08-13T14:30:00+00:00"}
109
+ ```
110
+
111
+ ### **Nested and Array Support**
112
+
113
+ ```python
114
+ # Nested datetime handling
115
+ user_data = {
116
+ "name": "John",
117
+ "profile": {
118
+ "created_at": datetime(2025, 8, 13, 10, 0),
119
+ "last_login": datetime(2025, 8, 13, 12, 0)
120
+ }
121
+ }
122
+ result = validate_to_dict(user_data, "name:string, profile:{created_at:datetime, last_login:datetime}")
123
+ # All nested datetime fields get timezone info automatically
124
+
125
+ # Array datetime handling
126
+ events = [
127
+ {"id": 1, "timestamp": datetime(2025, 8, 13, 9, 0)},
128
+ {"id": 2, "timestamp": datetime(2025, 8, 13, 10, 0)}
129
+ ]
130
+ result = validate_to_dict(events, "[{id:int, timestamp:datetime}]")
131
+ # All timestamps in array items get timezone info
132
+ ```
133
+
134
+ ### **Benefits**
135
+
136
+ - ✅ **Consistent API Responses**: All datetime fields include timezone information
137
+ - ✅ **Global Application Support**: Works correctly across all timezones
138
+ - ✅ **Frontend Compatibility**: JavaScript automatically handles timezone conversion
139
+ - ✅ **Zero Configuration**: Works automatically for all datetime fields
140
+ - ✅ **Performance Optimized**: Minimal overhead with maximum benefit
141
+
82
142
  ## 🎨 Decorators
83
143
 
84
144
  ### 4. `@returns_dict(schema_str)`
@@ -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.5"
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.5",
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",
@@ -64,7 +64,7 @@ from .integrations.json_schema import (
64
64
  # Note: Built-in presets moved to examples/ directory
65
65
  # Import them from simple_schema.examples.presets if needed
66
66
 
67
- __version__ = "0.1.0"
67
+ __version__ = "0.1.7"
68
68
  __author__ = "importal"
69
69
 
70
70
  __all__ = [
@@ -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.default is not None:
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 = None, min_val: Optional[Union[int, float]] = None,
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.default is not None:
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.default is not None:
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
- """Parse a single field with enhanced syntax support"""
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
- # Check for optional marker
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 field_str.endswith('?'):
146
- required = False
147
- field_str = field_str[:-1].strip()
148
-
149
- # Split field name and definition
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.default is not None:
491
+ if field.has_default:
367
492
  prop["default"] = field.default
368
493
 
369
494
  # Handle union types
@@ -15,6 +15,7 @@ Key Functions:
15
15
 
16
16
  import functools
17
17
  import uuid
18
+ from datetime import datetime, timezone
18
19
  from typing import Any, Dict, Type, Union, Callable, Optional, List
19
20
  import logging
20
21
 
@@ -30,6 +31,48 @@ except ImportError:
30
31
  logger = logging.getLogger(__name__)
31
32
 
32
33
 
34
+ def _ensure_timezone_aware_dict(data: Dict[str, Any]) -> Dict[str, Any]:
35
+ """
36
+ Ensure all datetime values in a dictionary have timezone information.
37
+
38
+ This function recursively processes dictionaries and lists to add UTC timezone
39
+ information to naive datetime objects, ensuring consistent API responses.
40
+
41
+ Args:
42
+ data: Dictionary that may contain datetime values
43
+
44
+ Returns:
45
+ Dictionary with timezone-aware datetime values converted to ISO format
46
+ """
47
+ if not isinstance(data, dict):
48
+ return data
49
+
50
+ result = {}
51
+ for key, value in data.items():
52
+ if isinstance(value, datetime):
53
+ # Add UTC timezone to naive datetime objects
54
+ if value.tzinfo is None:
55
+ value = value.replace(tzinfo=timezone.utc)
56
+ # Convert to ISO format string for consistent API responses
57
+ result[key] = value.isoformat()
58
+ elif isinstance(value, dict):
59
+ # Recursively process nested dictionaries
60
+ result[key] = _ensure_timezone_aware_dict(value)
61
+ elif isinstance(value, list):
62
+ # Process lists that may contain dictionaries or datetime objects
63
+ result[key] = [
64
+ _ensure_timezone_aware_dict(item) if isinstance(item, dict)
65
+ else item.isoformat() if isinstance(item, datetime) and item.tzinfo is None
66
+ else item.replace(tzinfo=timezone.utc).isoformat() if isinstance(item, datetime)
67
+ else item
68
+ for item in value
69
+ ]
70
+ else:
71
+ result[key] = value
72
+
73
+ return result
74
+
75
+
33
76
  def string_to_model(schema_str: str, name: Optional[str] = None) -> Type[BaseModel]:
34
77
  """
35
78
  Create Pydantic model from string schema.
@@ -218,13 +261,21 @@ def validate_to_dict(data: Union[Dict[str, Any], Any], schema_str: str) -> Union
218
261
  try:
219
262
  # Try Pydantic v2 RootModel style
220
263
  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()
264
+ # Return the validated array data with timezone-aware conversion
265
+ result_data = validated_instance.model_dump() if hasattr(validated_instance, 'model_dump') else validated_instance.dict()
266
+ # Process array items for timezone-aware datetime conversion
267
+ if isinstance(result_data, list):
268
+ return [_ensure_timezone_aware_dict(item) if isinstance(item, dict) else item for item in result_data]
269
+ return result_data
223
270
  except:
224
271
  # Fallback to Pydantic v1 style
225
272
  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__']
273
+ # Return the validated array data with timezone-aware conversion
274
+ result_data = validated_instance.model_dump()['__root__'] if hasattr(validated_instance, 'model_dump') else validated_instance.dict()['__root__']
275
+ # Process array items for timezone-aware datetime conversion
276
+ if isinstance(result_data, list):
277
+ return [_ensure_timezone_aware_dict(item) if isinstance(item, dict) else item for item in result_data]
278
+ return result_data
228
279
  else:
229
280
  # Handle different input types for object schemas
230
281
  if isinstance(data, dict):
@@ -236,11 +287,14 @@ def validate_to_dict(data: Union[Dict[str, Any], Any], schema_str: str) -> Union
236
287
  # Try direct validation
237
288
  validated_instance = TempModel(data)
238
289
 
239
- # Return as dictionary (use model_dump for Pydantic v2, fallback to dict for v1)
290
+ # Return as dictionary with timezone-aware datetime handling
240
291
  if hasattr(validated_instance, 'model_dump'):
241
- return validated_instance.model_dump()
292
+ result_dict = validated_instance.model_dump()
242
293
  else:
243
- return validated_instance.dict()
294
+ result_dict = validated_instance.dict()
295
+
296
+ # Ensure timezone-aware datetime conversion for consistent API responses
297
+ return _ensure_timezone_aware_dict(result_dict)
244
298
 
245
299
  except ValidationError as e:
246
300
  # Re-raise the original validation error
@@ -403,7 +457,7 @@ def get_model_info(model_class) -> Dict[str, Any]:
403
457
  Returns:
404
458
  Dictionary with model information including fields, types, and constraints
405
459
  """
406
- 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):
407
461
  raise ValueError("Input must be a Pydantic model class")
408
462
 
409
463
  info = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: string-schema
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: A simple, LLM-friendly schema definition library for converting string syntax to structured schemas
5
5
  Home-page: https://github.com/xychenmsn/string-schema
6
6
  Author: Michael Chen
@@ -151,6 +151,7 @@ String Schema takes human-readable text descriptions and converts them into stru
151
151
  - **🤖 LLM Data Extraction**: Define extraction schemas that LLMs can easily follow
152
152
  - **🔧 API Development**: Generate Pydantic models and OpenAPI docs from simple syntax
153
153
  - **✅ Data Validation**: Create robust validation schemas with minimal code
154
+ - **🌍 Timezone-Aware APIs**: Automatic UTC conversion for consistent datetime handling
154
155
  - **📋 Configuration**: Define and validate application configuration schemas
155
156
  - **🔄 Data Transformation**: Convert between different schema formats
156
157
 
@@ -175,6 +176,12 @@ print(user_dict) # {"name": "John Doe", "email": "john@example.com", "age": 25}
175
176
  user_model = validate_to_model(raw_data, "name:string, email:email, age:int?")
176
177
  print(user_model.name) # "John Doe" - Full type safety
177
178
  print(user_model.age) # 25 - Converted to int
179
+
180
+ # 🌍 Automatic timezone handling for datetime fields
181
+ from datetime import datetime
182
+ event_data = {"name": "Meeting", "start_time": datetime(2025, 8, 13, 14, 30)}
183
+ event_dict = validate_to_dict(event_data, "name:string, start_time:datetime")
184
+ print(event_dict) # {"name": "Meeting", "start_time": "2025-08-13T14:30:00+00:00"}
178
185
  ```
179
186
 
180
187
  ### 🎨 Function Decorators
@@ -31,6 +31,7 @@ docs/getting-started.md
31
31
  docs/pydantic-utilities.md
32
32
  docs/string-syntax.md
33
33
  docs/troubleshooting.md
34
+ examples/defaults_descriptions_demo.py
34
35
  examples/demo.py
35
36
  examples/pydantic_utility_demo.py
36
37
  string_schema/__init__.py
File without changes
File without changes
File without changes
File without changes