string-schema 0.1.6__py3-none-any.whl → 0.1.7__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/__init__.py CHANGED
@@ -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
@@ -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 = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: string-schema
3
- Version: 0.1.6
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
@@ -1,9 +1,9 @@
1
- string_schema/__init__.py,sha256=J0_B0TNO0AK_piEcT5gFFHllywjx16DTP59ps_Fp-WU,4052
1
+ string_schema/__init__.py,sha256=Vosc4ZtNqbdr06YwSbAYt16tfJxliONVEx5ekLXx5vM,4052
2
2
  string_schema/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
3
- string_schema/utilities.py,sha256=FjWKNHPVMFxUc04ITtt2eC4bI6ayLDYwr44fnBgN0-o,22351
3
+ string_schema/utilities.py,sha256=57fzXusitdYL2AFVitY0_3d8JSlnNGC5GEqw7tzd_YI,22388
4
4
  string_schema/core/__init__.py,sha256=jeukkZ1WoCSXrBafumHVH2GkCJ6QjnkH_jAr3DESFio,481
5
- string_schema/core/builders.py,sha256=AHDTqtCnSe5ZN-DwedisqXqktjgl6nypd-mebucjI3k,7809
6
- string_schema/core/fields.py,sha256=iLUR-w3pZCr7OkQHhb2DFkVLtObnpyJ_mEUdc0qAqXY,5534
5
+ string_schema/core/builders.py,sha256=DLloMAxeBSUSS_h7cqQ6RJPF4wnl4MyzdmJgMfj2M-M,7797
6
+ string_schema/core/fields.py,sha256=sqIG7zw1gm85zPnAgotgjsl9vXdki72JTgewrbV0asw,5690
7
7
  string_schema/core/validators.py,sha256=6inSZGou0zKpN47ttVWOxWsN-nPN3EewUNRacB9UUkA,8303
8
8
  string_schema/examples/__init__.py,sha256=wmdWey3ggt3yXG2XQygfHeghq-P31VdTkoqyki_qY1k,639
9
9
  string_schema/examples/presets.py,sha256=T7OQOsvVrPrzS2_1C6Hls17neeIGOBfVs0xOkZehaSQ,14642
@@ -11,14 +11,14 @@ string_schema/examples/recipes.py,sha256=bOX0zH-m5NVYFnfBIGzxag1BpNexe6OPJ55ssn0
11
11
  string_schema/integrations/__init__.py,sha256=yLRtfeVEDntULjMXKv_TWKXh860lKnQzCMMYcZvOr90,323
12
12
  string_schema/integrations/json_schema.py,sha256=cNr9chGc5RSfFLNXJCTH1ZOVd91OH9oeaAvf_Nqy8Zs,12627
13
13
  string_schema/integrations/openapi.py,sha256=bUHzJWOZEDtXs0R6493jAhA0-PrKwaUTkDW5UWE6-nI,15637
14
- string_schema/integrations/pydantic.py,sha256=hYrnl2hKcmbp7ypEINqn065dpLaCWxb39T1A8va1YrM,21818
14
+ string_schema/integrations/pydantic.py,sha256=m8cap2d8VHILKGnpqhh26IX1PDUFz2WPqBi1S1pUVq8,21810
15
15
  string_schema/integrations/reverse.py,sha256=qN8OCRjaH0nerZFrljAnvFLjZjo9gln2H-HBiaevLoI,9023
16
16
  string_schema/parsing/__init__.py,sha256=dFW68DqJIwP4w_aYaZMjjVK15X7tCv8A0SPqmIgGEjQ,411
17
17
  string_schema/parsing/optimizer.py,sha256=Ofte8Tb7sKmdEv7RrVIEBQ4Qqr-Whanp1vh8BakcOgw,8479
18
- string_schema/parsing/string_parser.py,sha256=-a_143fjNkxcRu24JdpqA6GltR7a_2AUXPNgHsbwYv4,24989
18
+ string_schema/parsing/string_parser.py,sha256=xsHMYdMEwGirQn0GJWsOy26LqVTangzMxz8BrQ5meEw,29230
19
19
  string_schema/parsing/syntax.py,sha256=RO3BIAnWfDBupowOnoJocHtAe-kwE-SgRWXKknVwGdg,8900
20
- string_schema-0.1.6.dist-info/licenses/LICENSE,sha256=wCD2KBeSlVSkJy0apl6zmVj04uw97UJhRRi-en_3Qfk,1065
21
- string_schema-0.1.6.dist-info/METADATA,sha256=L96Jgf9qVZ2K2LwLb5H7stwh684wy5vYFR2bze1yQII,18662
22
- string_schema-0.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
23
- string_schema-0.1.6.dist-info/top_level.txt,sha256=1uTmLPYIRrCDVxUW1fDFRMaWvGO48NRcGmbW4oq89I0,14
24
- string_schema-0.1.6.dist-info/RECORD,,
20
+ string_schema-0.1.7.dist-info/licenses/LICENSE,sha256=wCD2KBeSlVSkJy0apl6zmVj04uw97UJhRRi-en_3Qfk,1065
21
+ string_schema-0.1.7.dist-info/METADATA,sha256=NSS3KDt63-2VzhB6i_EYG8cN7N76ckOxL3GIReystGk,18662
22
+ string_schema-0.1.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
23
+ string_schema-0.1.7.dist-info/top_level.txt,sha256=1uTmLPYIRrCDVxUW1fDFRMaWvGO48NRcGmbW4oq89I0,14
24
+ string_schema-0.1.7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5